Colorpicker

A color value chosen from a gradient surface, a set of presets or a typed hex, with alpha and a format switch.

Accessibility

ColorPicker is half real controls and half divs. What it does for you, and the two props that fill the gaps.

What it renders

Half of ColorPicker is real controls and half is divs. Knowing which is which is most of the work.
  • The default trigger is a plain div. It has no role, no tab stop and no announced value.
  • The saturation area is a div too, with mouse and touch handlers only. The keyboard cannot reach it.
  • The hue and transparency sliders are real sliders with arrow-key support, but they have no label and announce only a number.
  • The hex, RGB and HSB fields are real inputs.
  • The clear button is a real button, named “Clear color” in English whatever the locale.
  • Preset swatches are divs with no role, tab stop or name. Their group headings can be reached, the swatches cannot.

Reaching it from the keyboard at all

Start with the trigger, since nothing in the panel matters until it can be opened.
  • Pass children when keyboard access matters. A Button as the trigger gives the picker a tab stop, a role and Enter and Space to open it.
  • With the default trigger, add tabIndex, role and aria-label on the picker, which passes them to the trigger. A real button is still better.
  • In the panel, Tab reaches the mode switch, the sliders, the format select, the fields, the clear button and each preset heading.
  • Saturation and brightness cannot be set by keyboard. The typed fields are the only full keyboard route.
  • Preset swatches cannot be reached by keyboard, so do not make them the only way to pick a color.

Naming it, and saying the value out loud

A swatch has no text, so a picker with nothing else announces nothing. Say what the color is for, and what it is set to.
  • Name the control for what the color is for. “Brand color”, not “Color picker”. If a visible label says it already, point aria-labelledby at it.
  • showText is how the value gets announced. It puts the value in the page as text. The swatch alone is a background, which screen readers do not read.
  • Where your product has a name for the color, show it with the function form of showText. “Ocean” is easier to hear than a hex read out character by character.
  • A custom trigger needs its own name and value text. children drops showText and any aria-* set on the picker.
  • Label the preset groups. The label is the only text that tells two blocks of swatches apart.

Color is never the only carrier

This applies to the picker, and to whatever the picked color is used for.
  • Without showText, the swatch alone shows the value, and a reader who cannot see it, or cannot tell two greens apart, gets nothing.
  • A picked color cannot be the only thing telling two items apart. A tag, chart series or calendar entry also needs a name, a shape or a pattern.
  • You cannot promise contrast for a color the reader chose. Keep your own text on a surface you control, or work out a readable text color from the chosen one.
  • Where the color carries meaning, say it in text too: “Urgent (red)”.

Contrast, and motion

  • The trigger's border keeps the swatch visible. A reader can pick the color of the page itself, and then only the border shows there is a control. Do not remove it.
  • The diagonal line of a cleared value and the checkerboard of a transparent one tell “nothing” from “white” by sight only, so screen readers need the text beside them.
  • Everything around the reader's color comes from your theme. Check contrast again when you change its seed colors.
  • The popup fade and slider transitions turn off with ThemeScope theme={{ token: { motion: false } }}, which readers who prefer reduced motion should get.