Color Picker
An accessible, composable color picker with a saturation area, hue and alpha sliders, eyedropper, swatches, and hex/rgb/hsl/oklch output.
#3b82f6
Installation
Usage
A headless-ish set of parts rather than one fixed widget: ColorPicker owns the color state and
every other part reads from it, so you arrange the surface you actually need. Nothing renders unless
you ask for it — drop ColorPickerAlpha if your colors are always opaque, drop
ColorPickerFormatSelect if you only ever emit hex.
import {
ColorPicker,
ColorPickerAlpha,
ColorPickerArea,
ColorPickerEyeDropper,
ColorPickerFormatSelect,
ColorPickerHue,
ColorPickerInput,
ColorPickerPreview,
ColorPickerSwatch,
ColorPickerSwatches,
} from "@/components/ui/color-picker";
export function BrandColor() {
const [color, setColor] = useState("#3b82f6");
return (
<ColorPicker value={color} onValueChange={setColor}>
<ColorPickerArea />
<div className="flex items-center gap-3">
<ColorPickerEyeDropper />
<div className="flex min-w-0 flex-1 flex-col gap-2">
<ColorPickerHue />
<ColorPickerAlpha />
</div>
<ColorPickerPreview />
</div>
<ColorPickerFormatSelect />
<ColorPickerInput alpha />
<ColorPickerSwatches>
<ColorPickerSwatch value="#ef4444" />
<ColorPickerSwatch value="#22c55e" />
<ColorPickerSwatch value="#3b82f6" />
</ColorPickerSwatches>
</ColorPicker>
);
}State
ColorPicker is controlled with value / onValueChange and uncontrolled with defaultValue.
onValueChange hands you a CSS color string serialized in the active format, so the value you get
back is the value you can paste straight into a stylesheet.
<ColorPicker defaultValue="oklch(0.62 0.19 259)" defaultFormat="oklch" />Internally the color is stored as HSVA, not as the string. That is what lets you drag into pure black and drag back out onto the hue you started from — a picker that round-trips through hex loses the hue the moment brightness hits zero.
format / defaultFormat / onFormatChange control the output format the same way. Switching
format fires onValueChange too, since the value string changes with it.
Pass name to submit the value with a plain HTML form:
<ColorPicker name="accent" defaultValue="#3b82f6" />Parts
| Part | Notes |
|---|---|
ColorPicker | State owner and context provider. Accepts disabled and name. |
ColorPickerArea | Saturation and brightness plane. |
ColorPickerHue | Hue slider, 0–360. |
ColorPickerAlpha | Alpha slider over a checkerboard. |
ColorPickerPreview | Current color chip. Size it with className. |
ColorPickerEyeDropper | Screen sampler. Renders nothing where the EyeDropper API is unavailable. |
ColorPickerFormatSelect | Segmented format control. Narrow it with formats={["hex", "rgb"]}. |
ColorPickerInput | Format-aware fields — a hex box, or one field per channel. alpha adds one. |
ColorPickerSwatches | Preset container. |
ColorPickerSwatch | One preset. value takes any parseable color string. |
Popover trigger
The item ships the panel, not the trigger, so it composes with whatever overlay you already use:
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>
<span className="size-4 rounded" style={{ backgroundColor: color }} />
{color}
</PopoverTrigger>
<PopoverContent className="w-64">
<ColorPicker value={color} onValueChange={setColor}>
<ColorPickerArea />
<ColorPickerHue />
<ColorPickerInput />
</ColorPicker>
</PopoverContent>
</Popover>Custom parts
useColorPicker exposes the same context the built-in parts use, so you can add your own without
forking the component:
import { useColorPicker } from "@/components/ui/color-picker";
function CopyColor() {
const { value, description } = useColorPicker();
return (
<Button variant="ghost" onClick={() => navigator.clipboard.writeText(value)}>
Copy {description}
</Button>
);
}It returns hsva, setColor, rgb, hex, cssColor, value, format, setFormat,
description, and disabled.
Color utilities
parseColor, formatColor, and describeColor are exported for use outside the picker.
parseColor reads #rgb, #rgba, #rrggbb, #rrggbbaa, rgb(), hsl(), and oklch() in both
the legacy comma syntax and the modern space/slash syntax, and returns null rather than throwing
on input it cannot read.
formatColor(parseColor("#3b82f6")!, "oklch"); // "oklch(0.6231 0.188 259.81)"
describeColor(parseColor("#3b82f6")!); // "vibrant blue"Accessibility
The saturation plane is backed by two visually hidden range inputs — one per axis — following the
pattern React Aria uses for its ColorArea. Screen readers get a real value to announce, keyboard
users get a real control to focus, and mobile assistive tech gets its native slider gestures. Arrow
keys drive both axes from either input, so focus order never changes what the keys do.
Every control announces a plain-language color description next to its numeric value — "62%, dark vibrant blue" rather than "182, 96, 38". Descriptions are derived in OKLCH, whose lightness is perceptually even across hues, so "dark" means the same thing on yellow as it does on blue.
| Key | Action |
|---|---|
← → | Saturation, or the slider value, by one step |
↑ ↓ | Brightness in the area |
Shift + arrow | Ten steps |
Home End | Minimum / maximum |
Page Up Page Down | Ten steps |