Sidebar nav item — accessibility

Keyboard

  • Every row is a real control: a <a href> when it has a route, otherwise a <button> (NavGo). Tab reaches them in reading order, Enter follows, Space activates the button form.
  • The split parent is two separate tab stops — body then chevron — so folding is reachable without a pointer.
  • The group divider is a <button aria-expanded>; Enter folds and unfolds its block.
  • The rail toggle in the footer is a button with aria-expanded and an aria-label that says what it will do.
  • There is no roving focus and no arrow-key movement inside the nav; it is a list of links, which is what the ARIA authoring practices ask for.

ARIA

  • The active row carries aria-current="page" — that is the programmatic truth; the .active class is only paint.
  • In the rail the label is hidden with CSS, so each row carries title and aria-label with the same text — the accessible name never disappears with the label.
  • Fold state is on the control that folds (aria-expanded on .as-grp, .as-row-chev, .as-sub-chev).
  • The mobile hamburger in the top bar owns aria-expanded and aria-controls="as-side-nav", so the off-canvas sidebar is announced as its target.
  • The colour tile .sg and the wand are decorative; the label carries the meaning.

Focus

  • app-shell.css declares no focus style of its own and — importantly — does not switch the outline off: the rows are <button>/<a>, so they take the foundation ring from ds.css (outline: 2px solid var(--accent); outline-offset: 1px on :focus-visible).
  • Folding a group does not move focus; the chevron keeps it.
  • The nav scrolls inside a scroll area; a focused row below the fold is scrolled into view by the browser.

Contrast

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

  • Row label --ink-700 on the sidebar surface = 12.55:1 light, 11.29:1 dark.
  • Active row --ink-900 on --active-surface — above 12:1 in both themes; the active state is carried by both fill and weight of colour, not by colour alone.
  • Row icon --ink-500 — mid-tone, above the 3:1 non-text minimum in light theme.
  • Count pill: .acrm-pill.blue / .amber ink on their tint, both above 4.5:1 (the same pills used across the app).
  • Focus ring --accent = 4.44:1 light, 3.83:1 dark — passes the 3:1 non-text minimum.

Known limits

  • The active state is a fill plus a colour change; there is no shape or icon difference, so a user who cannot see the fill relies on aria-current alone (which is correct, but the visual redundancy is thin).
  • No disabled style exists for a row that permissions have made unavailable — such rows are removed from the model instead of being shown greyed out.
  • The rail hides labels below 56px of width; users who need the text must expand the sidebar, there is no tooltip on hover for the rail rows beyond the native title.
  • There is no skeleton for the navigation while modules and segments load — the nav simply renders fewer rows until the data arrives.

Source: docs/app-shell/sidebar-nav-item.a11y.md · GitLab