Record select — accessibility

Keyboard

  • Trigger (button): Tab focuses it; Enter or Space toggles the panel (native click); disabled removes it from the tab order. With anchorRef there is no trigger — the host's element opens the panel and the panel is mounted only while open.
  • On open, focus moves to the search input on the next animation frame (after the panel is positioned — a hidden panel cannot be focused).
  • Typing filters in memory over label, sub and keywords, with diacritics folded ("novak" finds "Novák").
  • Esc closes the panel and returns focus to the anchor or trigger; the key is consumed (preventDefault), so page-level Esc shortcuts such as "close record" do not fire and a panel inside a dialog never closes the dialog with it.
  • Rows are real buttons: Tab moves from the search input through the rows; Enter or Space picks. Single mode closes on pick; multi keeps the panel open.
  • Arrow keys, Home and End move focus inside the panel — they come from the canonical Popover behaviour shared by every menu in the system.
  • Enter in the search fires onCreate when the footer's quick-create row is offered.
  • Click outside the anchor and the portalled panel closes it (capture-phase mousedown).
  • The clear "×" is tabindex="-1" — mouse only (limit).

ARIA

  • Trigger: aria-haspopup="listbox", aria-expanded, aria-label from ariaLabel; no role="combobox" (a plain button).
  • Search input carries an aria-label (the search placeholder), so it is named without relying on the placeholder alone.
  • List: div[role="listbox"] with aria-multiselectable in multi mode; rows button[role="option"] with aria-selected.
  • Sections are role="group" with aria-label, so the grouping is announced even where the visible name is not drawn (a single section draws no name).
  • The indicator is decorative — aria-selected carries the state; the mark is decorative too (the name is in .nm).
  • Clear: span[role="button"][aria-label="Vymazat"].
  • Panel carries data-rr-keep-open so the record rail treats clicks inside the portal as inside the editor.
  • No aria-controls from trigger or input to the panel.

Focus

  • Trigger :focus-visible = foundation ring outline: 2px solid var(--accent); outline-offset: 1px; while open the wrapper adds the accent border + 3px 16% halo to the trigger.
  • Search input: outline: 0 — no ring on the input itself; the open trigger halo is the only indicator (the input is visually "inside" a fresh panel).
  • Row buttons get the foundation outline when tabbed to.
  • Closing (Esc, pick, outside click) returns focus to the anchor or trigger.

Contrast

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

  • Trigger filled --ink-900 on --surface-100 = 17.59:1 (light), 14.53:1 (dark).
  • Trigger placeholder / disabled --fg-muted = 2.85:1 (light), 3.63:1 (dark); disabled on --surface-75 2.75:1 / 3.82:1.
  • Search input text --ink-900 14px = 17.59:1 / 14.53:1; placeholder --fg-muted 2.85:1 / 3.63:1.
  • Row name .nm --ink-900 = 17.59:1 / 14.53:1; on hover surface 17.0:1 / 13.09:1.
  • Row secondary .em --fg-secondary 300 12px on white = 3.93:1 (light) — below AA; 6.2:1 (dark).
  • Section name var(--label) in --fg-muted on the panel = 2.85:1 (light), 3.63:1 (dark) — below AA in both themes. The Art Director asked for the lighter tone on 12 September 2026 (ACRM-227) with these numbers in front of him: the label has to step back behind the rows it separates. It is a decorative group label — assistive technology reads the group from role="group" + aria-label on .acrm-recordselect-grp, not from this text — so nothing depends on reading it. Measured on the panel surface, which is the lighter of the two grounds.
  • Indicator --accent on white = 4.44:1 (light), 3.83:1 (dark).
  • Empty text --fg-muted 13px = 2.85:1 / 3.63:1.
  • Panel border --border-strong 1.38:1 / 1.55:1; search hairline --border ≈ 1.1:1.

Known limits

  • The clear "×" is unreachable by keyboard.
  • Rows are individual tab stops as well as arrow targets.
  • Section names, secondary text and all --fg-muted text are below AA 4.5:1 in the light theme.
  • Nothing announces that the pinned block at the top is "what you already picked" — the order is a visual convention.

Muted and warning options (acrm !4348)

  • A muted option keeps the name at --fg; the dimming is visual only (mark at 50 %) and the reason is real text in the option's accessible name, so screen readers hear „Bohumil Král, dovolená do 6. 10.“.
  • subTone: 'warn' text is part of the accessible name as well; colour is never the only signal.

Source: docs/select/record-select.a11y.md · GitLab