Guide
The keyboard and screen readers
Colour pickers are among the worst-served widgets on the web: the most popular vanilla one has had an issue titled "Accessibility" open since 2018. This one was built the other way round — every control is a real one.
The square
A saturation-and-brightness square is two dimensions, and role="slider" is one. A single slider
element cannot carry the aria-valuenow its own role requires when there are two numbers to report
— which is the open complaint against the most used picker on npm.
So the square is a role="group", and inside it are two real <input type="range">, one per axis.
Each has its own label and value, and both carry an aria-valuetext naming the pair, so whichever
one has focus, the whole colour is read out: "Saturation, slider, Saturation 77%, brightness 100%."
They are taken out of the layout rather than hidden from the accessibility tree, so they take focus
normally; the square shows that focus with :focus-within, and the thumb shows where the colour is.
| Key | What it does |
|---|---|
| ← → | saturation, by 1 |
| ↑ ↓ | brightness, by 1 |
| Shift + any arrow | by 10 |
| Home / End | the ends of the axis you are on |
| Page Up / Page Down | that axis, by 10 |
Both axes take every arrow, so nobody has to work out which one they are on.
Everything else
| Part | What it is |
|---|---|
| Hue, opacity | Real <input type="range">, with the browser's own keyboard behaviour and focus ring. |
| The value field | A real <input> with a label. Enter applies the colour and does not submit the form. Something unparseable sets aria-invalid and leaves the colour alone. |
| Format, eyedropper, copy | Real <button type="button"> with names. |
| Swatches | Real buttons in a group, each named "Use #ffbe0b". |
| Copying, and the gamut warning | Announced through a role="status" live region. |
Tab order runs the way the widget reads: square, hue, opacity, value, format, tools, swatches.
Seeing it
- The thumb's ring is whichever of black and white reads better on the colour underneath, so it stays visible on yellow and on navy.
@media (forced-colors: active)gives the widget system borders and aHighlightfocus ring.- Focus is never indicated by colour alone.
- Nothing animates that
prefers-reduced-motionwould object to.
And the point of contrastAgainst
The picker can tell you, as you pick, that the colour is 3.5 : 1 on white and therefore not enough for body text. That is an accessibility feature aimed at the person choosing the colour rather than the person using the picker — and it is the one that keeps the rest of the site readable.