Rail — accessibility
Covers the whole card: the panel, its rows, editing a value in place and fields with several values (merged 14 Sep 2026, RD-1).
Keyboard
- Every interactive part of the rail is a real control: action buttons, section headers that fold, value hover-zones (
.rr-ev, focusable), the pencil where it is a button, the copy button, „Zobrazit všechny hodnoty“ and the relation rows. - Tab walks actions → fields (top to bottom) → relations. Each value of a multi-value field is its own stop.
- Enter on a focused value opens the editor; Escape cancels and returns focus to the value. Enter in a single-line editor commits; blur commits too, and a latch makes sure Enter-then-blur cannot save twice or Escape-then-blur resurrect a cancelled edit. A multi-line editor (
textarea) keeps Enter for new lines and commits on blur. - A value that is a link or an action follows it on Enter; its editor is reached through the pencil, which is a real button in the tab order.
- Folding a section keeps focus on the header; folded content is removed from the tab order.
- There is no arrow-key movement between rows — the rail is a list of fields, not a grid.
- Multi-value editor: Tab walks grip → value → type → remove for every row and ends on the add input; Enter in the add input appends a value. Remove reacts to
mousedownso the popover cannot close under the pointer — Enter/Space on the focused button still works. Reordering is drag-only (see Known limits). - Below 720px the editor is a bottom sheet:
.phe-sheetfoot's "Done" is an ordinary focusable<button class="acrm-btn primary">, reachable by Tab like the rest of the popover's rows, and the scrim closes the sheet on tap. There is no sheet-specific Escape behaviour beyond what the popover already does (onEscape ?? onClose).
ARIA
- The pair is not a definition list: the label is a
<span>next to the value, and the reading order (label then value) carries the pairing. Label and icon are not focusable. - Values that lead somewhere are
<a>or<button>with atitlefromopenLabel(Otevřít web,Zavolat), so the action is announced instead of being guessed from the value. - The copy affordance is a button named by the field (
Kopírovat Telefon); it is only rendered when the rail has anonCopyhandler, so there is never a dead control. - A non-editable row has no editing affordance at all, so nothing promises an interaction that will not happen. The empty placeholder is text inside the value slot — a screen reader reads the invitation, not a blank.
- Read mode is a focusable element with the value as its text; edit mode replaces it with a labelled control.
- Relation rows are references (
EntityRef); their remove control carriesaria-labelnaming the item. - The status dot
.rr-dotisrole="img"witharia-labelandtitlefromRRRelationItem.dot.title("Přijal(a)", "Možná", "Odmítl(a)") — it is the only carrier of the answer, so it must never be rendered without a title. - The icon slot
.rr-statusholds whatever the caller passes; the caller owns its accessible name (delivery ✓✓ comes from Mail with its own title). - Multi-value: the remove button is named for its value (
Odebrat +420 605 123 456); the editor setsaria-busywhile a change is being written; the dimming of further values is visual only — every value is read in full. - The sheet keeps the popover's own
role="dialog"andaria-label(fromtitle/label) regardless of viewport —MultiValuePopoverdoes not swap roles for the mobile variant. The grab handle and the scrim are botharia-hidden="true", purely decorative. - Nothing announces a successful save, a failed save, or an added / removed value (see Known limits).
Focus
.rr-evsetsoutline: noneand supplies its own ring —box-shadow: 0 0 0 2px var(--accent)— because the row is 34px and a 2px outline with offset would touch the neighbouring row.- The open editor
.rr-editdraws an accent border plus a 3px 20 % accent halo and no outline. - Cancelling or committing returns focus to the value, so the user keeps their place in the rail. Focus never moves on hover; the pencil and copy icons appear, but nothing takes focus.
- Everything else in the rail (buttons, links, pencil, copy, editor controls) keeps the foundation ring from
ds.css. - The decorative pencil is a
<span>and is not focusable; on action rows, where it is the only way to edit, it is a real button. - Row actions (
.rr-acts, e.g. „Odebrat vazbu“, accept / reject on a CRM change row) are hidden withopacity, nevervisibility: they stay in the Tab order and keyboard focus reveals them via:focus-within(acrm !4532, 3 Oct 2026 — before, they were unreachable from the keyboard). A contract test guards both.rr-actsand the list's.il-acts. - The sheet variant does not trap or move focus any differently from the desktop popover — no additional focus management runs for it; Tab can still walk out of the sheet into whatever the popover is portalled into (the host dialog, or the page behind the scrim on
<body>), same as the popover's own known limits.
Contrast
Computed from packages/ui/src/tokens/tokens.css:
- Value
.rr-row .v--ink-900on the panel = 17.59:1 light, 14.53:1 dark. - Label
.rr-row .kuses the--labeltoken in a muted tone — below AA for small text, and it is the muted half of a pair whose value carries the meaning. - Empty placeholder
.v.ph--fg-muted= 2.85:1 light, 3.63:1 dark — below AA; it is an invitation, not content. - The editing ring, the editor border and the value link use
--accent= 4.44:1 light, 3.83:1 dark — passes the 3:1 non-text minimum. - The green saved check uses the positive tone; it is a confirmation, never the only carrier of state.
- Second and further values of a multi-value field and the value type
.phe-typeare--fg-muted— the same limit, deliberately, so the primary value reads first. This is the card's main accessibility cost. - Pencil, copy, grip and remove glyphs
--fg-muted— below the 3:1 non-text minimum in light theme; they are secondary affordances next to a value that is itself the target. - Sheet grab handle
.phe-grab--line-200— decorative andaria-hidden, no text or target sits on it, so it carries no contrast requirement. Sheet scrim.phe-scrimuses--ov-scrim, the same overlay tone as every other modal scrim.
Touch
- Every icon action in a row (copy, pencil, description chevron) has a hit area of at least 44 × 44 px on
hover: none; copy and pencil sit 30 px apart so their areas never overlap (see usage → Touch). - Nothing is reachable only by hover: copy and the chevron of a clamped description are always visible on touch, reordering goes through a menu instead of a drag grip.
Known limits
- The "unknown" dot is
--ink-300on the rail surface — well below the 3:1 non-text minimum. It is deliberate (it must read as "nothing came back yet", not as a state), and the meaning is in the tooltip andaria-label, not the colour; on an 8px dot the earlier--ic-limewas indistinguishable from green (AD, 6 Sep 2026). - No live region: a save is confirmed only by a green check that fades after ~1.1 s, a failed save reverts silently in the row (the screen's toast is the only announcement, and it must be one), and adding or removing a value is silent.
- There is no "saving" state — a slow save shows nothing until it settles.
- Labels, placeholders and secondary values sit at the
--fg-mutedlimit in light theme. - The pencil appears on hover; on touch it is permanently visible only for action rows, so on a touch device an ordinary row is edited by tapping the value itself, which is not signposted. Copy is hover-revealed on desktop and permanently visible below 720px and on touch.
- A long label is cut by the 108px column with an ellipsis and no tooltip.
- Multi-value: reordering by grip has no keyboard alternative, and removing a row drops focus to the body.
- The sheet's grab handle (
.phe-grab) is a purely visual affordance — there is no swipe-to-dismiss gesture wired to it; the sheet only closes through the "Done" button or a scrim tap.
Source: docs/record-rail/rail.a11y.md · GitLab