# ColorPicker

A colour, chosen or typed.

- Group: Forms
- Import: `import { ColorPicker } from '@misoto22/design'`
- Page: https://ui.misoto22.com/components/color-picker/
- Related: field, input, popover

## When to reach for it

A person is choosing the colour. A colour that is merely being SHOWN is a swatch, and a set of fixed brand colours is a RadioGroup — a picker offers sixteen million answers to a question with six.

## Anatomy

- **Trigger** (required) — The closed control: a swatch, then the value as text, in the same box as Input and Select. Named by its label AND by its own value, so a reader hears “Brand colour, #a78bfa” rather than either half.
- **Swatch** — The colour over a checkerboard, so a half-transparent value reads as transparent rather than as a paler colour.
- **Notation strip** — Hex, OKLCH and Display P3, as a single-value ToggleGroup. It changes what onValueChange emits, not what the colour is — and hex and P3 are bounded, so switching to one of them fits the colour to that gamut on the way out.
- **Field** (required) — The plane: chroma across, lightness up. Each ROW is normalised to the most chroma that exists at that lightness and hue, so the whole surface is reachable instead of a lens of colour inside bands of clipped duplicates. Underneath it are two real sliders rather than key handlers on a canvas, which is what gives it arrows, Home, End and an announced position.
- **Hue track** — A ramp taken at the lightness and chroma already chosen, not a generic rainbow — so the strip shows the hues of THIS colour rather than of some other one.
- **Opacity track** — Transparent to the current colour, over the same checkerboard as the swatch.
- **CSS box** — The value as text. Accepts hex, rgb(), hsl(), oklch() and color(display-p3 …) in both syntaxes, and paints itself invalid on anything else.

## Best practices

### Do

- Wrap it in a Field. The trigger is a button, which a label binds to and clicks through — one of the few composites where that works without help.
- Pass the notation you want back. The panel emits in whatever notation the value arrived in until somebody changes it in the strip, so a defaultValue of "#a78bfa" keeps a consumer in hex.
- Reach for this over <input type="color"> when the colours are being TUNED. The native picker works in HSV, where a row of constant lightness visibly darkens as it saturates — so a reader building a palette is fighting the instrument. OKLCH is the space where two colours at the same height genuinely match.

### Don’t

- Do not pass a named colour. Hex, rgb(), hsl(), oklch() and color(display-p3 …) parse; "rebeccapurple" does not, and the box will show it as invalid. Resolving names needs a table of every CSS keyword or a live DOM, and a picker that takes some names and not others is worse than one that takes none.
- Do not read the emitted string as a fixed notation. It is whatever the strip is set to, so a consumer that slices a "#" off the front breaks the first time a reader picks OKLCH.
- Do not use it to pick text or background colour and call the result accessible. Nothing here measures contrast; a picker that lets a reader choose #eeeeee for body copy is doing exactly what it was asked.

## Accessibility

- label is required. The trigger shows a value, and a value is not a name.
- The plane is a group of two real sliders — Chroma and Lightness — each announced with a percentage, so the 2D surface is operable and reported rather than merely clickable.
- The focus ring is drawn on the plane, because the sliders that take the focus are visually hidden and the browser’s own ring is clipped away with them.
- Alpha is doubled by a checkerboard everywhere it is shown, so transparency is not carried by lightness alone.

## Keyboard

- Enter / Space — Opens the panel.
- ← / → — Moves the focused axis or track by one step.
- Home / End — Jumps that axis or track to its ends.
- Escape — Closes the panel; focus returns to the trigger.

## ColorPicker

A colour, chosen or typed. The panel works in OKLCH, and that is the reason to reach for this rather than `<input type="color">`. In HSV — which is what the native picker and most libraries use — dragging along a row of constant "lightness" walks through colours the eye reads as getting darker, so a reader tuning a palette is fighting the instrument. OKLCH's lightness is the one a person sees, so two colours at the same height on the plane genuinely match, and the hue strip stays at the lightness already chosen instead of showing a rainbow that belongs to some other colour. The plane is normalised to the gamut ROW BY ROW: its right edge is the most chroma that exists at that lightness and hue, so the whole surface is reachable rather than a lens of colour inside a field of clipped duplicates. **What it accepts.** Hex, `rgb()`, `hsl()`, `oklch()` and `color(display-p3 …)`, in both syntaxes. Named colours are not accepted: resolving them needs a table of every CSS name or a live DOM, and a box that takes "rebeccapurple" but not "papayawhip" is worse than one that takes neither. **Keyboard.** The plane is a group of two real sliders — chroma across, lightness up — so the arrows move it and a screen reader announces where it is. That is the part a 2D canvas usually leaves out, and leaving it out makes the control unusable rather than merely awkward.

### Props

- `label` (required) — `string`. Names the control. Required — the trigger shows a colour, and a colour is not a name. Announced together with the value, the way `Select`'s is: a reader hears "Brand colour, #a78bfa". Inside a `Field` with a label, that label is used and this one is not repeated.
- `value` — `string`. The colour, as any absolute CSS colour string.
- `defaultValue` — `string` default `'#000000'`.
- `onValueChange` — `(value: string) => void`. Fires with a CSS colour string in whichever notation the panel is set to — so the notation a caller passes in is the notation they get back until somebody changes it in the panel.
- `disabled` — `boolean`.
- `invalid` — `boolean`. Paints the resting border with `--danger` and reflects `aria-invalid`.
- `className` — `string`.
- `id` — `string`. The TRIGGER's id — the element a label points at. A `Field` sets it.
- `aria-describedby` — `string`. Ids of the copy describing the control. A `Field` sets it from hint, error and description.
- `aria-invalid` — `boolean | 'true' | 'false'`. The spelling a form library sets; read together with `invalid`.
- `aria-required` — `boolean`. Announced on the trigger. A `Field` sets it from `required`.

## Example — default

```tsx
import { ColorPicker, Field } from '@misoto22/design'

<div className="flex w-full max-w-xs flex-col gap-5">
  <Field label="Brand colour">
    <ColorPicker label="Brand colour" defaultValue="#a78bfa" />
  </Field>
  <Field label="Accent" hint="Passed in as OKLCH, so it comes back as OKLCH.">
    <ColorPicker label="Accent" defaultValue="oklch(0.72 0.16 145)" />
  </Field>
</div>
```
