Skip to content
misoto22 design

Forms

ColorPicker

A colour, chosen or typed.

Usage

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.
TSX
import { ColorPicker } from '@misoto22/design'

Notes

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.

Anatomy

ColorPicker anatomy
ElementDescription
TriggerrequiredThe 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.
SwatchThe colour over a checkerboard, so a half-transparent value reads as transparent rather than as a paler colour.
Notation stripHex, 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.
FieldrequiredThe 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 trackA 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 trackTransparent to the current colour, over the same checkerboard as the swatch.
CSS boxThe 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.

Examples

default

The panel works in OKLCH, which is the whole argument for it over the native picker. In HSV — what <input type="color"> and most libraries use — a row of constant lightness visibly darkens as it saturates, so somebody building a palette is fighting the instrument. Here two colours at the same height on the plane genuinely match, and the hue strip is taken at the lightness already chosen rather than being a rainbow belonging to some other colour. The plane is a pair of real sliders under a painted canvas, so the arrows move it and a screen reader is told which axis it is on.

Passed in as OKLCH, so it comes back as OKLCH.

Keyboard

ColorPicker keyboard interactions
KeyDoes
EnterSpaceOpens the panel.
Moves the focused axis or track by one step.
HomeEndJumps that axis or track to its ends.
EscapeCloses the panel; focus returns to the trigger.

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.