Pickers and overlays
Color Picker
A custom color picker with a swatch, two-dimensional color area, hue slider, and editable hex value.
When to use it: Use it when the browser color input is too limited and people need a polished, consistent opaque sRGB picker.
Example
Theme preview
Expensive previews can render at lower priority.
import { useDeferredValue, useState } from "react";
import {
ColorArea,
ColorAreaThumb,
ColorPicker,
ColorPickerInput,
ColorPickerPopover,
ColorPickerTrigger,
ColorPickerValue,
ColorSlider,
ColorSwatch,
Label,
} from "@comp0/react";
type ThemePreviewProps = {
color: string;
};
function ThemePreview({ color }: ThemePreviewProps) {
return (
<div
aria-label="Theme preview"
className="mt-3 overflow-hidden rounded-lg border border-zinc-950/10 bg-white dark:border-white/10 dark:bg-zinc-900"
>
<div className="h-2" style={{ backgroundColor: color }} />
<div className="flex items-center justify-between gap-3 p-3">
<div>
<p className="text-sm font-medium text-zinc-900 dark:text-zinc-100">Theme preview</p>
<p className="text-xs text-zinc-500 dark:text-zinc-400">
Expensive previews can render at lower priority.
</p>
</div>
<span
aria-hidden="true"
className="size-8 shrink-0 rounded-full border border-zinc-950/10 dark:border-white/10"
style={{ backgroundColor: color }}
/>
</div>
</div>
);
}
export function Example() {
const [color, setColor] = useState("#0d9488");
const previewColor = useDeferredValue(color);
return (
<ColorPicker
as="div"
className="flex max-w-xs flex-col gap-1.5"
name="accent"
value={color}
onChange={setColor}
>
<Label className="text-base font-medium text-zinc-900 sm:text-sm dark:text-zinc-100">
Accent color
</Label>
<ColorPickerTrigger
aria-label="Choose accent color"
className="flex h-11 items-center gap-2 rounded border border-zinc-950/10 bg-white px-2.5 text-left text-base text-zinc-800 outline-teal-600 focus-visible:outline-2 sm:h-9 sm:text-sm dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-100 dark:outline-teal-400"
>
<ColorSwatch className="size-6 shrink-0 rounded border border-zinc-950/15 shadow-inner dark:border-white/20" />
<ColorPickerValue className="font-mono tabular-nums" />
</ColorPickerTrigger>
<ColorPickerPopover
aria-label="Accent color picker"
placement="bottom start"
offset={4}
className="w-72 rounded-lg border-0 bg-white p-3 shadow-lg ring-1 ring-zinc-950/10 dark:bg-zinc-900 dark:shadow-none dark:ring-white/10"
>
<ColorArea
aria-label="Saturation and brightness"
className="relative h-40 w-full touch-none overflow-hidden rounded-md border border-zinc-950/10 bg-[linear-gradient(to_top,#000,transparent),linear-gradient(to_right,#fff,transparent)] outline-teal-600 focus-within:outline-2 dark:border-white/10 dark:outline-teal-400"
style={{ backgroundColor: "hsl(var(--comp0-color-area-hue) 100% 50%)" }}
>
<ColorAreaThumb className="absolute size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-transparent shadow ring-1 ring-zinc-950/25 dark:ring-zinc-950/50" />
</ColorArea>
<ColorSlider
aria-label="Hue"
channel="hue"
className="mt-4 h-3 w-full cursor-pointer appearance-none rounded-full bg-[linear-gradient(to_right,#ef4444,#eab308,#22c55e,#06b6d4,#3b82f6,#a855f7,#ec4899,#ef4444)] outline-teal-600 focus-visible:outline-2 [&::-moz-range-thumb]:size-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-zinc-950 [&::-moz-range-thumb]:shadow [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-zinc-950 [&::-webkit-slider-thumb]:shadow dark:outline-teal-400 dark:[&::-moz-range-thumb]:bg-white dark:[&::-webkit-slider-thumb]:bg-white"
/>
<label className="mt-4 flex items-center gap-2">
<span className="text-base text-zinc-500 sm:text-sm dark:text-zinc-400">Hex</span>
<ColorPickerInput className="min-w-0 flex-1 rounded border border-zinc-950/10 bg-white px-2.5 py-2 font-mono text-base text-zinc-950 outline-teal-600 focus-visible:outline-2 sm:text-sm dark:border-white/10 dark:bg-zinc-950 dark:text-zinc-50 dark:outline-teal-400" />
</label>
</ColorPickerPopover>
<ThemePreview color={previewColor} />
</ColorPicker>
);
}Anatomy
Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.
A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.
ColorPicker
Shared opaque sRGB color value and open-state provider. Does not add a DOM element.
ColorPickerTrigger
Button that shows the current swatch and opens the picker. Owns a DOM element.
ColorSwatch
Visual chip filled with the current color. Owns a DOM element.
ColorPickerValue
Current lowercase hex value. Owns a DOM element.
ColorPickerPopover
Floating dialog surface for precise color selection. Owns a DOM element.
ColorArea
Two-dimensional saturation and brightness control backed by hidden native range inputs. Owns a DOM element.
ColorAreaThumb
Visual marker positioned at the chosen saturation and brightness. Owns a DOM element.
ColorSlider
Native hue range control synchronized with the color area. Owns a DOM element.
ColorPickerInput
Editable hex input synchronized with the picker value. Owns a DOM element.
Step by step
- 1
Add the main part
Start ColorPicker with a ColorPickerTrigger containing ColorSwatch and ColorPickerValue.
- 2
Add the supporting parts
Put ColorArea with ColorAreaThumb, a hue ColorSlider, and ColorPickerInput inside ColorPickerPopover.
- 3
Make the behavior clear
Use a lowercase opaque hex value such as "#0d9488". Keep the picker value immediate and defer only expensive previews derived from it; pass name to ColorPicker when the value belongs in a form. This release has no alpha or wide-gamut color support.
Exampletsx <ColorPicker defaultValue="#0d9488"><ColorPickerTrigger><ColorSwatch /><ColorPickerValue /></ColorPickerTrigger><ColorPickerPopover><ColorArea><ColorAreaThumb /></ColorArea><ColorSlider channel="hue" /><ColorPickerInput /></ColorPickerPopover></ColorPicker>
Keyboard
- ↵
- Opens the picker from the trigger.
- Space
- Opens the picker from the trigger.
- Esc
- Closes the popover and returns focus to the trigger.
- ←→↑↓
- Adjust the focused saturation, brightness, or hue range.
- HomeEnd
- Move the focused color-area axis to its minimum or maximum.
- PgUpPgDn
- Make a larger change on the focused color-area axis.
Forms and accessibility
ColorPicker submits one hidden input under name with its lowercase #rrggbb value. It represents opaque sRGB only: alpha and wide-gamut colors are not supported.
Accessibility checklist
- Give the trigger an accessible name when its visible swatch and hex value do not say what the color controls.
- ColorArea exposes two hidden native range inputs, one each for saturation and brightness, with two-dimensional slider descriptions; keep the visual thumb and value visible.
- Keep the hex input available for precise entry, and do not use color alone to convey the selected value or its meaning.
- Keep controlled picker updates urgent so its native ranges and announced value stay synchronized. Use useDeferredValue for expensive page, canvas, or theme previews instead.
API reference
import { ColorPicker, ColorPickerTrigger, ColorPickerPopover, ColorArea, ColorAreaThumb, ColorSlider, ColorSwatch, ColorPickerValue, ColorPickerInput } from "@comp0/react";ColorPicker
Context onlyShared opaque sRGB color value and open-state provider.
| Prop | Type | Description |
|---|---|---|
value | "#rrggbb" | Controlled lowercase opaque hex value. |
defaultValue | "#rrggbb" | Initial lowercase opaque hex value. |
onChange | (value: string) => void | Receives the next lowercase hex value. |
open / defaultOpen | boolean | Controlled or initial open state of the color popover. |
onToggle | (open: boolean) => void | Receives the next open state. |
name | string | Submission name for the hidden hex input. |
form | string | Associates the hidden value with a form by id. |
disabled / invalid / required | boolean | Field-wide states. |
ColorPickerTrigger
DOM elementButton that shows the current swatch and opens the picker.
| Prop | Type | Description |
|---|---|---|
aria-label | string | Names the trigger when its visible content is not enough. |
ColorSwatch
DOM elementVisual chip filled with the current color.
ColorPickerValue
DOM elementCurrent lowercase hex value.
ColorPickerPopover
DOM elementFloating dialog surface for precise color selection.
| Prop | Type | Description |
|---|---|---|
aria-label | string | Defaults to the English "Color picker"; pass a translation. |
placement | PopoverPlacement | Trigger side to open on, such as "bottom start"; flips when there is no room. |
offset | number | Pixel gap between the trigger and the surface. |
ColorArea
DOM elementTwo-dimensional saturation and brightness control backed by hidden native range inputs.
| Prop | Type | Description |
|---|---|---|
aria-label | string | Names the saturation and brightness control. |
ColorAreaThumb
DOM elementVisual marker positioned at the chosen saturation and brightness.
ColorSlider
DOM elementNative hue range control synchronized with the color area.
| Prop | Type | Description |
|---|---|---|
channel | "hue" | Selects the hue channel; this release supports hue only. |
ColorPickerInput
DOM elementEditable hex input synchronized with the picker value.
| Prop | Type | Description |
|---|---|---|
aria-label | string | Names the hex input when no visible text does. |
Style hooks
Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.
[data-open]on ColorPickerTrigger, ColorPickerPopover
- The color popover is open.
:popover-openon ColorPickerPopover
- Native pseudo-class equivalent.
[data-value]on ColorPicker
- A color is selected.
[data-disabled]on ColorPickerTrigger
- The picker is disabled.