Persona

Swatch Picker

Inputs · 3 props · Updated Sep 22, 2026

Swatch Picker renders a list of colors as round buttons and reports the one that was pressed. It has three props, all of them required, and no state: the palette as an array of strings, the current value, and a change handler.

  • Swatch grid the whole control, wrapping its swatches into rows. It has no label of its own, which is what Form Field is for.
  • Swatch one color, as a button. Selection cannot be shown in color here, so it is shown as a ring around the chosen one.

The grid wraps into rows at the width of whatever contains it, each swatch is a 24px button filled with its color, and the selected one is marked with a ring around it. There is no swatch component and no per-swatch options, because colors is an array of strings and the string is everything the picker knows about a color.

The palette is the whole control. colors is a fixed array and there is no field to type into, which suits a choice the product has already narrowed: brand colors, or the accent on a hosted flow. A swatch picker has no way to accept a typed value, so a text input does that job, either beside the picker or in place of it.

A swatch picker also holds a single value. For choosing several colors at once there is no PDS control, and building one is building a component.

Order the array by hue or lightness, and keep it that way

Section titled “Order the array by hue or lightness, and keep it that way”

Mixing brand colors with arbitrary ones empties the constraint the picker exists to express. Order the array by hue or by lightness and keep it that way between renders: the reader is scanning for a color and there is nothing else in the grid to scan by, so a familiar position is the only landmark on offer.

The strings in colors go straight into background-color. White has to be special-cased to be visible at all, and PDS does that by comparing the string to #FFFFFF and swapping the white border for a hairline one. The comparison is on the string, so #fff, white and an rgb() triple all render a white circle with a white border on a white surface. The source says the same, and says it is a hack that handles exact white only, so near-white colors have the problem too.

Each swatch is a <button> with the color string as its title and nothing inside it, so its accessible name is “#3F48FD”. There is no prop for naming a color, and PDS’s own test file records the problem as unsolved. A reader using a screen reader gets the palette as a list of hex codes in the order you passed them.

So a swatch picker should not be the only route to a color that matters.

The picker is a plain div with no role: no radiogroup, no roving tabindex, no arrow keys. Tab therefore stops on all ten swatches of a ten-color palette, which is one more reason to keep a palette short.

Nothing marks a swatch unavailable, either. There is no disabled on the picker or on a swatch, and no way to take one out of the tab order, so a color that must not be chosen has to be left out of colors.

A form field gives the picker a visible label and nothing more

Section titled “A form field gives the picker a visible label and nothing more”

An aria-label passed to Swatch Picker lands on that plain div, where a generic element takes no accessible name and the label has nowhere to attach. Wrap the picker in Form Field for a visible label above it. Note what that does and does not buy: the label is visible text, and with no input underneath it there is nothing for it to be programmatically associated with.

Section titled “Print the token name where precision matters”

PDS already puts the hex in a browser tooltip on every swatch, so a picker whose reader needs Core / Purple / 600 has to print it outside the grid. The field label and its helper text are the place for that, and the rules for both are on Writing.

NameType
valuerequired

string

onChangerequired

(color: string) => void

colorsrequired

string[]

Also accepts every prop of Omit<ComponentPropsWithoutRef<‘div’>, ‘onChange’>.

Was this page helpful?