Multi-step dialog — accessibility
Everything in dialog.a11y.md holds for every step (Escape order, focus trap, inert background, scroll lock). This page covers what the steps add.
Keyboard
- Search step: the query field has focus on open.
↓/↑move the cursor over the rows (wrapping),Enterpicks the row under the cursor — a record or Create person "…". The cursor resets to the first row for a new query. Moving the mouse over a row also moves the cursor there (filtered against syntheticmousemoveevents that arrow-key scrolling triggers, so keyboard use never gets fought by the mouse). - The footer's primary button (Select record / Create person,
MorphText) is a normal tab stop after the query field and is disabled when nothing is active — it duplicatesEnterfor anyone who tabs to it instead of pressing Enter in the field. - Escape closes the whole flow from any step (it is one window).
⌘↵creates the deal only in the detail step. - After a pick, focus lands in the detail's title field (prefilled with the picked name); after Create person, in the New person name field.
ARIA
- The query field is
role="combobox"witharia-haspopup="listbox",aria-expandedwhen there are rows, andaria-activedescendantpointing at the row under the cursor; each section is anItemListwithrole="listbox"andaria-label= the section name, rows arerole="option". - Each row's accessible name includes its
content(company/domain) andmeta(Person/*Company*) slots — both render as plain text, notaria-hidden, so the type is announced along with the row, not only conveyed visually. - The footer's
↑ ↓ Navigatehint (span.lead) isaria-hidden="true"— keyboard legends aren't read, same convention askbdhints insideButton. The cursor itself is announced througharia-activedescendant, not through this hint. - The window keeps
role="dialog"; the search step's<Dialog bare>carries the flow's title as its accessible name even though no header renders visually. The handed-over New person step is its own dialog with its own name.
Focus
- The panel is not unmounted between in-window steps, so focus never falls to
<body>there; the incoming step'sautoFocus(the query field) takes it. - Hand-over between dialogs: the leaving dialog returns focus to the trigger only when focus fell to
<body>; the arriving dialog moves focus into itself on mount, so the user lands in the new step. - Focus ring: the foundation
:focus-visiblering on every control (query field, footer button); the cursor row uses theItemListcursor state, not a focus ring — the input keeps DOM focus throughout keyboard navigation of the list.
Contrast
- Rows, section labels, the
contentandmetaslots use the palette's tokens (--fg,--label/--fg-secondary) on--surface-100— same values as the ⌘K palette. - The motion carries no information: nothing is conveyed by the animation alone (the spring is the same physical curve for every reshape in the system, RULES.md §11).
Touch
- On phone the footer's
↑ ↓ Navigatehint is hidden and the primary ("Select record" ↔ "Create person") is<Button touch>: 44px high, full width (acrm !4007, 27 Sep 2026). Any other footer button gets the 44px footer floor fromresponsive.css. See Button › Touch target on phone.
Known limits
- The step change is not announced by a live region; screen-reader users learn about it from the focus move into the new step.
prefers-reduced-motion: the window swaps size without a transition (springAnimatereturnsnull) and the content appears without a fade; the footer'sMorphTextlabel swaps with no cross-fade either.- The search step lists at most 8 rows (
recentSearch/searchlimit); there is no "load more" in the picker yet.
Source: docs/dialog/multi-step-dialog.a11y.md · GitLab