Task list — accessibility

Keyboard

  • The state glyph .tl-state is a real <button>: Tab reaches it, Space or Enter finishes or reopens the task.
  • The title opens the task with Enter where the screen wires onOpen.
  • The disclosure for subtasks and the add row are buttons and are reachable in order.
  • Reordering by drag has no keyboard alternative (onReorder is pointer-only). Every order it can produce stays reachable without a pointer through Řadit — the drag only writes the Vlastní order, it does not gate anything the list cannot otherwise show.

ARIA

  • Rows are list items with their controls inside; the state button carries aria-pressed (done or not).
  • The state button's icon is decorative, so its title — the state name plus what the click does (e.g. „Blokováno — označit jako hotové") — is the button's accessible name; nothing equivalent exists as visible row text since 15 Sep 2026 (readable id and state-name text were removed from the row).
  • A done task is struck through, its state glyph is filled, and aria-pressed="true" carries it programmatically — the strike-through is never the only signal.
  • The priority glyph is decorative (aria-hidden); priority is not announced in the row.
  • On an AI suggestion row (.tl-row.sug) the whole state slot is decorative: .tl-state.sug carries aria-hidden="true" and is not a button (a suggestion has no state to toggle yet) — unlike a real task row, where the same slot is always a live, focusable button.
  • Counts (comments, subtasks) are numbers next to icons and are read with them.
  • The suggested group's actions appear on hover or focus within the row, so keyboard users reach them by tabbing.
  • Neither loading nor a failed load is announced; both are visible states (SkeletonTasks, ErrorState with a retry button).
  • Host behaviour (/tasks, ACRM-273): the Návrhy entry chip is a real <button> with aria-pressed={sugView}, reachable and toggleable by keyboard like any other chip on the bar. The „Seskupit" chip is the same NavSelect pattern already used for „Řadit" — no separate a11y contract.

Focus

  • Everything interactive in the row is a <button> or an input, so the foundation ring from ds.css applies; task-list.css does not switch outlines off.
  • The assignee link (.tl-person) declares its own :focus-visible, because it is a dashed-underline text link inside a dense row.

Contrast

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

  • Title in ink on the row surface — above AA in both themes; a done title drops to muted with a strike-through.
  • Due date: neutral muted, .now amber, .over --critical-ink = 5.37:1 light — the tone is carried by colour, and the word („dnes“, a date) carries it too.
  • Counts and the assignee name sit at the muted limit (2.85:1 light).
  • The state glyph takes the bucket tone (--ic-blue open and done, --ic-amber in progress, --ic-grey blocked); a done task is a filled --ic-blue circle with a --fg-on-accent tick — the same pair as the old accent checkbox, 4.44:1 light, 3.83:1 dark.
  • The row's hover fill (--hover-surface, replacing the 15 Sep 2026-removed hairline) is a background change only — it carries no text and needs no contrast ratio of its own.

Mobile (≤720px)

  • The state button .tl-state stays visible and reachable — only the linked-entity slot .tl-rel is removed from the DOM (display: none) below 720px; nothing that carries an accessible name disappears.
  • AI suggestion actions (.tl-sugact) render at rest instead of on hover/focus — the touchscreen has no hover, so keyboard/AT users and touch users reach the same always-visible buttons; the app-only responsive layer also gives them an explicit touch height.
  • Assignee as mark + initials (.tl-person.ini) is role="img" with aria-label = the full name (acrm !4114): the mark itself is aria-hidden and the initials would be spelled out („Š S"). On a phone the initials are hidden, so the label is the only name — never drop it.

Touch (acrm !4069)

Suggestion actions are never hover-only on touch: visible at (hover: none) and ≤ 720 px, 44 px tall (calc(var(--sp-7) + var(--sp-1))), each a real <button> with a text label (no icon-only × ✓ any more — acrm !4067).

Known limits

  • Drag reordering is pointer-only — but pointer, not mouse: since 16 September 2026 the gesture runs on pointer events (320ms long press on touch), so a finger can reorder too. Scrolling survives: touch-action: none is set only on the row that is actually lifted, and a press that travels more than 8px before the hold elapses is a scroll, not a drag.
  • The lifted row is marked visually only (.dragging); there is no live region and no aria-grabbed — assistive technology hears nothing about the move until the list re-renders.
  • Priority is visual only: the scale glyph is hidden from assistive technology and the row has no text for it.
  • Loading and error states are silent for assistive technology.
  • Secondary text (counts, assignee, dates) sits below AA in light theme.
  • In narrow and full mode the assignee is a badge plus initials whose full name lives in a title — reachable, but only on hover or through assistive technology; only wide mode shows the full name as visible text.
  • The state name is exposed only through the state button's title/accessible name, never as visible row text — a sighted mouse user gets it only on hover, same limit as the assignee initials above.
  • stateName is a plain string supplied by the caller — if the host passes a non-empty but meaningless value (e.g. a raw internal code), assistive technology reads exactly that, uncorrected.
  • Switching into or out of the Návrhy view moves nothing and announces nothing: focus stays on the toggle chip and the swapped list content is silent for assistive technology, the same limit as loading/error above.

Source: docs/task-list/task-list.a11y.md · GitLab