Skip to content
Devix Open Source

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 a Highlight focus ring.
  • Focus is never indicated by colour alone.
  • Nothing animates that prefers-reduced-motion would 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.

Updated 12 Sep 2026