Skip to content

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.

On this page

Example

Theme preview

Expensive previews can render at lower priority.

Color Picker.tsxtsx
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.

  1. ColorPicker

    Shared opaque sRGB color value and open-state provider. Does not add a DOM element.

  2. ColorPickerTrigger

    Button that shows the current swatch and opens the picker. Owns a DOM element.

  3. ColorSwatch

    Visual chip filled with the current color. Owns a DOM element.

  4. ColorPickerValue

    Current lowercase hex value. Owns a DOM element.

  5. ColorPickerPopover

    Floating dialog surface for precise color selection. Owns a DOM element.

  6. ColorArea

    Two-dimensional saturation and brightness control backed by hidden native range inputs. Owns a DOM element.

  7. ColorAreaThumb

    Visual marker positioned at the chosen saturation and brightness. Owns a DOM element.

  8. ColorSlider

    Native hue range control synchronized with the color area. Owns a DOM element.

  9. ColorPickerInput

    Editable hex input synchronized with the picker value. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Start ColorPicker with a ColorPickerTrigger containing ColorSwatch and ColorPickerValue.

  2. 2

    Add the supporting parts

    Put ColorArea with ColorAreaThumb, a hue ColorSlider, and ColorPickerInput inside ColorPickerPopover.

  3. 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

Importtsx
import { ColorPicker, ColorPickerTrigger, ColorPickerPopover, ColorArea, ColorAreaThumb, ColorSlider, ColorSwatch, ColorPickerValue, ColorPickerInput } from "@comp0/react";

ColorPicker

Context only

Shared opaque sRGB color value and open-state provider.

PropTypeDescription
value"#rrggbb"Controlled lowercase opaque hex value.
defaultValue"#rrggbb"Initial lowercase opaque hex value.
onChange(value: string) => voidReceives the next lowercase hex value.
open / defaultOpenbooleanControlled or initial open state of the color popover.
onToggle(open: boolean) => voidReceives the next open state.
namestringSubmission name for the hidden hex input.
formstringAssociates the hidden value with a form by id.
disabled / invalid / requiredbooleanField-wide states.

ColorPickerTrigger

DOM element

Button that shows the current swatch and opens the picker.

PropTypeDescription
aria-labelstringNames the trigger when its visible content is not enough.

ColorSwatch

DOM element

Visual chip filled with the current color.

ColorPickerValue

DOM element

Current lowercase hex value.

ColorPickerPopover

DOM element

Floating dialog surface for precise color selection.

PropTypeDescription
aria-labelstringDefaults to the English "Color picker"; pass a translation.
placementPopoverPlacementTrigger side to open on, such as "bottom start"; flips when there is no room.
offsetnumberPixel gap between the trigger and the surface.

ColorArea

DOM element

Two-dimensional saturation and brightness control backed by hidden native range inputs.

PropTypeDescription
aria-labelstringNames the saturation and brightness control.

ColorAreaThumb

DOM element

Visual marker positioned at the chosen saturation and brightness.

ColorSlider

DOM element

Native hue range control synchronized with the color area.

PropTypeDescription
channel"hue"Selects the hue channel; this release supports hue only.

ColorPickerInput

DOM element

Editable hex input synchronized with the picker value.

PropTypeDescription
aria-labelstringNames 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-open

on ColorPickerPopover

Native pseudo-class equivalent.
[data-value]

on ColorPicker

A color is selected.
[data-disabled]

on ColorPickerTrigger

The picker is disabled.

Keep exploring