Popover — accessibility
Keyboard
- The menu follows the WAI-ARIA menu pattern (acrm !3560, 15 Sep 2026). Opening a panel with
role="menu"or"listbox"moves focus to the first item, or to the panel itself when it has no items. - Up / Down move between rows, Home / End jump to the first and last (since ccd9350dc).
- Rows are
<button>, so Enter and Space activate them. Selecting a row closes the panel and returns focus to the trigger. - Escape closes the panel and returns focus to the trigger. Inside a modal
Dialogthe popover consumes Escape in the capture phase, so the dialog underneath stays open. - The panel also closes when the anchor's scroll container scrolls or the window resizes.
ARIA
- Panel:
role="menu"by default (overridable throughrole), named byariaLabel. - Rows carry the role the screen gives them:
menuitem,menuitemradioormenuitemcheckboxwitharia-checked— state must be in the attribute, not only in the.chkglyph, which is hidden withvisibilitywhen off. - The trigger is expected to carry
aria-haspopup="menu"andaria-expanded; the component does not set them on the caller's element. - The empty line
.acrm-popover-emptyis plain text and is not announced when it replaces the list. - The panel deliberately has no heading element — the accessible name replaces it.
Focus
- Foundation ring from
ds.css:outline: 2px solid var(--accent); outline-offset: 1pxon:focus-visible— it coversbuttonand[role='menuitem*'], so rows are visibly focused once focus reaches them. - With no items the panel itself takes focus, so a keyboard user always lands inside the menu.
- Focus returns to the trigger after Escape or a selection — but only when it has fallen to
<body>. A row whose action mounts its ownautoFocusfield (for example „Přejmenovat sloupec“ in the segment screen) keeps that focus; the check is the same oneDialoguses. - Focus on OPEN lands one frame later (acrm !3956, 27 Sep 2026): the panel is
visibility: hiddenuntilplace()positions it, andfocus()on a hidden element is a no-op — so the open-focus effect runs in the next animation frame (with cleanup). A search field inside the panel takes focus before the first item. jsdom ignores visibility, so only a real browser (Playwright) proves this. - The trigger that menus wrap in a non-focusable
<span>anchor (PickerMenu, MenuButton, NavSelect) gets focus back throughfocusAnchor: the anchor itself, or its first focusable trigger (button/link/tabindex); an anchor holding a text field (combobox) is left alone, it manages its own focus (acrm !3932). - A click outside (on the scrim) closes the panel without returning focus: the user has pointed somewhere else, and focus stays where they clicked.
Contrast
Computed from packages/ui/src/tokens/tokens.css:
- Row label
.nm--ink-900on--surface-100= 17.59:1 light, 14.53:1 dark. - Icon slot
.ic--fg-muted= 2.85:1 light, 3.63:1 dark — below the 3:1 non-text minimum in light theme. - Check
.chk--accent= 4.44:1 light, 3.83:1 dark — passes 3:1. - Destructive row
.dangeruses--critical-ink= 5.37:1 light, 5.80:1 dark — passes AA. - Disabled row: the label is dimmed below AA by design; the row is also removed from the tab order.
- Empty line
--fg-muted= 2.85:1 light — below AA for text. - Panel border
--borderon--surface-100: hairline, below the 3:1 non-text minimum; the--shadow-popelevation does the separating.
Known limits
- No focus management and no arrow keys: the menu is effectively unreachable by keyboard unless it contains a field that autofocuses.
- The trigger's
aria-haspopup/aria-expandeddepend on the caller; the component cannot enforce them. - The empty and loading line is silent for assistive technology.
- Muted icons and the empty line sit below the contrast minimums in light theme.
Source: docs/popover/popover.a11y.md · GitLab