Item list — accessibility
Keyboard
- A row that opens something is a real
<button>(or an<a>when it has an href), so Tab reaches it and Enter or Space opens it. - A static row is a
<div>and is deliberately not focusable — nothing promises an interaction that does not exist. - Lists that carry their own keyboard cursor (mail, queues) mark the focused row with the
cursorstate, which is distinct fromactive(the row the screen is showing). Both can be visible at once and they mean different things. - There is no arrow-key handling inside the component; a host that wants a cursor implements it and paints it with
il-cursor. On arole="listbox"container the component itself animates the highlight (.il-cursor-bar, sliding on a spring, acrm !3997) once the host has movedil-cursorto the new row — the host still owns which row is the cursor, the component only owns how the highlight travels there.
- Reordering (
onReorder): Alt+↑ / Alt+↓ moves the focused row by one place; focus stays on the moved row and anaria-live="polite"region (.acrm-sr) announces the new position („Krypto přesunuto na 3. místo z 6“). The grip isaria-hidden— the row itself is the control, so keyboard and screen-reader users never need it.
ARIA
- The container is
role="list"and every row sits inrole="listitem"— the row itself is a button, and an element has only one role, hence the wrapper. role="listbox"withoptionitems exists as an opt-in for the ⌘K palette, where the list is part of a combobox and a screen reader must hear "list with N options". It is a lead's decision (D-M3-7), not the canon.- The lead slot is decorative when it is an icon; when it is a checkbox it is a real input with its own label.
lockedrows are dimmed and lead nowhere; the state is visual, so the row must also say why in its content when that matters.- Nothing announces that the list grew after lazy loading.
Tags
- The
tagsslot is plain text in pills: no role, not focusable, nothing to announce beyond the words. A tag that is clickable (TaskListchip) is a real<button>inside the pill. - Tone and the leading icon of a pill (Project label) are not announced — the project name must say it all. Do not use a bare icon pill.
- On
lgthe tags are moved to a separate line by CSS only; the DOM order stays title · date · tags, which is the reading order a screen reader gets. Clipped pills (overflow: hiddenwhen they run out of room on desktop) are still in the DOM and in the row’stitle; on the phone nothing is clipped, the line wraps. - Hairlines and the group bar
.tl-grp.is-embeddedare decoration; the group header keeps its own heading semantics from the host.
Focus
- Rows are buttons or links, so the foundation ring from
ds.cssapplies (outline: 2px solid var(--accent); outline-offset: 1px);item-list.cssdoes not switch it off. - The
cursorstate is a background, not a focus ring — a host that moves the cursor with arrow keys must also move real focus, or expose it witharia-activedescendant. In arole="list"container that background is painted on the row itself; in arole="listbox"container it is the shared.il-cursor-barbehind the rows, and the row's own background stays transparent so the bar shows through.
Contrast
Computed from packages/ui/src/tokens/tokens.css:
- Title
.il-tin ink on the page surface = well above AA in both themes. - Byline and content
.il-by/.il-cand the meta column use muted tones —--fg-mutedis 2.85:1 in light theme, below AA for text. This is the deliberate cost of "hierarchy by colour alone". readlowers the tone further, so an already-seen row is the quietest thing in the list.lockeddims the whole row.- Focus ring
--accent= 4.44:1 light, 3.83:1 dark.
Known limits
- Hierarchy is carried by colour alone: for a user who cannot tell the tones apart, title, byline and meta read as one line.
- Secondary text sits at the muted contrast limit, and
read/lockedgo below it on purpose. - Lazy loading is silent — no live region announces new rows.
- The keyboard cursor is a host responsibility; the component only paints it.
prefers-reduced-motion: reduceis respected for therole="listbox"cursor: both the cursor bar's spring and the listbox's cross-fade (ilListIn) turn off, so the cursor and the list appear instantly instead of travelling or fading (acrm !3997). Nothing else in Item list carries motion to begin with.
Source: docs/item-list/item-list.a11y.md · GitLab