Components › Forms & input › Choice › Color picker

Color picker

canon

Grid of colour swatches (radiogroup, arrows, optional 'no colour' row) and its canonical wrapper: popover on desktop, sheet on the phone, InSheet panel inside a dialog.

acrm !4337 (one swatch grid instead of four copies: EventLabelPicker, EventTypeFilter, MailLabelDialog use ColorPicker; SegmentIconPicker keeps its own panel and uses ColorSwatches for SEGMENT_TONES — validator exception), !4347 (phone: swatches span the width, 44 px targets, segment icon tiles capped at --tap-min), !4354 (stories + usage/a11y), !4363 (a11y: 'Bez barvy' is a radio of the same group with a check on the right; roving tabindex — one Tab stop on the chosen option; focus ring and selection ring read apart). Canon 1. 10. 2026 (AD) after before/after app screenshots (1440 + 390, light/dark) and the States story. Custom hex colour in the segment picker unchanged; dropping it is a separate owner decision.

Used in 6 files · 5 areas · 6× — components/calendar, components/mail, components/meetings, components/segment, pages/TymyPage

Keyboard

  • The grid is a role="radiogroup" of <button role="radio">. Arrow keys move focus by one swatch (left, right) or one row (up, down); Home and End jump to the first and last. The move does not wrap and does not select — Enter or Space on the focused swatch picks it, which closes the panel.
  • Every swatch is a real <button> and a Tab stop. The group does not use a roving tabindex, so Tab walks all 24 swatches (a known limit, below).
  • On open, focus moves to the checked swatch, or to the first one when nothing is chosen, so a keyboard user starts where the current value is.
  • Escape closes the panel and nothing else: when the picker is opened from a dialog, the dialog stays open. Focus returns to the trigger.
  • The "Bez barvy" row is a <button> under the grid, one Tab stop after it; Enter or Space picks "no colour".
  • The foundation focus ring (2 px --accent, 1 px offset) applies to every swatch and is not switched off. It reads apart from the selection ring (canvas gap + --ink-900), so a focused swatch that is not chosen cannot be mistaken for the value.

ARIA

  • The group carries role="radiogroup" and an aria-label that names what is chosen (Barva štítku); on a phone the same text is the sheet title. The panel is role="dialog" with the same name.
  • Each swatch is role="radio" with aria-checked (true for the chosen colour only) and a name in both aria-label and title — the colour is never conveyed by its fill alone. Names are the palette names (Tomato, Sage, …) and Barva <tone> for tones.
  • The "Bez barvy" row is a button with aria-pressed, true while nothing is chosen. It is not a swatch and not a radio of the group: "no colour" is the absence of a value, and the row sits outside the radiogroup.
  • The swatch fill is decorative to assistive technology; the name is the content.

Selection, not a check mark

  • The chosen swatch is shown by a ring, not by a check mark inside it. A white check on the light tones had a contrast of 2.1:1 (amber), below the 3:1 required of a graphic object. The ring is drawn against the panel (--bg-canvas gap, then --ink-900), not against the swatch, so it holds on all 24 tones and in both themes.
  • Selection does not rely on colour alone: the ring is a shape difference.

Touch

  • On a phone the swatch stays 24 px but the touch target is 44 px (--tap-min) through an invisible ::after, centred on the swatch. The row pitch is 48 px, so neighbouring targets never overlap.
  • The sheet has a grab handle, a title and a full-width Hotovo, and tapping the backdrop closes it — Escape and a tap outside are not enough on a phone.

Contrast

Computed from packages/ui/src/tokens/tokens.css and the palette:

  • The selection ring is --ink-900 on the panel surface — above 3:1 in both themes.
  • The swatch itself is not text; the 24 palette colours are data (Google Calendar), and each one is told apart by name, not only by hue.
  • The "Bez barvy" label is ink on the panel surface, as every menu row.

Known limits

  • All 24 swatches are Tab stops; a roving tabindex (one stop, arrows inside) would shorten the walk and is not built.
  • Arrow keys stop at the ends of the grid; they do not wrap.
  • Colour names are the English Google names on purpose; a screen reader in Czech reads them with Czech phonetics.
  • Several palette colours are close in hue (for example Beetroot and Cherry Blossom); users with colour-vision differences rely on the name or the order, which follows the spectrum.

Source: docs/choice/color-picker.a11y.md · GitLab