Tab strip — accessibility

Keyboard

  • Tabs are real <button role="tab"> inside role="tablist": Tab moves focus into the strip, Enter and Space switch.
  • Single tab stop (roving tabindex): Tab lands on the active tab. ←/→ move focus with wrap-around, Home/End jump to the ends; arrows do not switch — Enter or Space does. Activation is manual, because a panel loads its data when it mounts, so switching on every arrow would fire a request per keystroke. Arrows with Alt, Ctrl, Meta or Shift are left to the browser (NAV-3, 11 Sep 2026).
  • The overflow chevrons are aria-hidden and only clickable when the edge is on — they are a pointer affordance, not a keyboard one. Keyboard users reach hidden tabs by tabbing, which scrolls them into view.

ARIA

  • The strip is role="tablist", each tab role="tab" with aria-selected.
  • Optional panelId: every tab gets id="<panelId>-tab-<key>" and the active tab gets aria-controls; the consumer marks the panel role="tabpanel" with aria-labelledby. Only the active tab points at the panel, because only its panel is rendered. Wired in Trash and Project detail; not in Record tabs (the strip and the panels live in different components) or Meeting detail.
  • The count .cnt is plain text inside the button, so it is read as part of the tab's name ("Úkoly 3") — which is the intended reading.
  • Nothing announces that the content below changed after a switch.

Focus

  • .acrm-tab is a <button> with role="tab", so the foundation ring from ds.css applies (outline: 2px solid var(--accent); outline-offset: 1px) — the selector list covers both button and [role='tab'], and the tabs CSS does not switch the outline off.
  • Focus follows the click; switching does not move focus elsewhere.
  • A tab scrolled out of view is scrolled back by the browser when it takes focus.

Contrast

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

  • Inactive tab label: muted ink on the page surface — readable, but the active tab is the one carrying weight.
  • Active tab: ink label plus the 2px accent underline (--accent on --surface-100 = 4.44:1 light, 3.83:1 dark) — the state is carried by both colour and the underline, not by colour alone.
  • Count .cnt: muted pill; the number repeats in the accessible name, so it is not the only carrier.
  • Fade edges are decorative gradients over the background; they carry no information that the chevron does not.

Known limits

  • aria-controls is not wired where the panel lives outside the strip's component.
  • No disabled state at all (neither prop nor CSS).
  • The overflow chevrons are pointer-only.
  • Switching a tab is silent for assistive technology.

Source: docs/tabs/tab-strip.a11y.md · GitLab