Components › Forms & input › Select › Picker menu

Picker menu

canon

Anchored menu of items with optional check marks and icons.

Slots added 27 Sep 2026 (acrm !3909, wave D): separator rows `{ key, sep: true }` with role=separator (edge/double separators dropped, same rule as Menu button), per-item `ariaLabel` for rows whose visible text is shorter than the full sentence, and a clear row (`clearLabel`/`clearIcon`/`onClear`) — role=menuitem, no check, NOT danger (clearing a value is reversible; red is only for deleting data). No group headings. Registry repaired 17 Oct 2026 (review F6): added exported PickerMenuPanel to react[].

Used in 51 files · 36 areas · 64× — components/AppShell, components/FilterBar, components/ai-employees, components/assistant, components/calendar, components/callwrapup …

Keyboard

  • The trigger is supplied by the caller; it must be a button so Enter/Space fire the click that the anchor span handles. A non-focusable trigger (div, span) makes the menu mouse-only.
  • Open menu: focus stays on the trigger; Tab walks the menuitemradio buttons; Enter or Space picks and closes.
  • No arrow navigation, Home/End or type-ahead; no focus trap.
  • Esc closes (window keydown in <Popover>); scrim mousedown, scroll of an anchor scroll-parent and resize close as well; scrolling inside the panel does not.
  • disabled only removes the anchor's click handler — the caller must also disable the trigger button.

ARIA

  • Shell: role="menu" with aria-label={header} — the name is invisible by design (no eyebrow in menus).
  • Rows: button[role="menuitemradio"] + aria-checked for the current value; icons (<Icon>) are aria-hidden; .stagedot is a colour-only span — the stage name is in .nm, so colour is not the only cue.
  • The accessible name is the row's name (or ariaLabel); the full name is also the row's title — no grey trailing tag (owner 28 Sep 2026, ACRM-375–377).
  • The trigger must carry aria-haspopup="menu" and aria-expanded={open} itself (Nav select does; a bespoke trigger has to add them).
  • No aria-controls from trigger to menu.

Focus

  • Trigger and rows use the foundation ring outline: 2px solid var(--accent); outline-offset: 1px (button, [role='menuitemradio']).
  • Focus is not moved into the menu on open and not restored to the trigger on pick or Esc (the row unmounts → body).

Contrast

Computed from packages/ui/src/tokens/tokens.css:

  • Row name --ink-900 on --surface-100 = 17.59:1 (light), 14.53:1 (dark); on hover --hover-surface 17.0:1 / 13.09:1.
  • Icon slot --fg-secondary = 3.93:1 (light) / 6.2:1 (dark) — non-text, passes 3:1.
  • Tag --fg-muted 11px = 2.85:1 (light) / 3.63:1 (dark) — below AA 4.5:1.
  • Check --accent = 4.44:1 (light) / 3.83:1 (dark).
  • Danger row --critical #e5484d on white = 3.91:1 (light) — below AA for 13px text; 4.35:1 (dark); on --critical-subtle #ffebeb hover ≈ 3.4:1.
  • Disabled row opacity: 0.4 ≈ 2.6:1 (light).
  • Stage dot 9px: colour from var(--ic-*) — not a text element; the name carries the meaning.

Known limits

  • Menu keyboard support is Tab-only; focus is neither trapped nor restored.
  • Trigger a11y attributes depend on the caller (render prop).
  • Tag (11px --fg-muted) and danger text are below AA 4.5:1 in light theme.
  • disabled does not disable the trigger visually or for assistive tech.

Source: docs/select/picker-menu.a11y.md · GitLab