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-expandedand anaria-labelthat 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.activeclass is only paint. - In the rail the label is hidden with CSS, so each row carries
titleandaria-labelwith the same text — the accessible name never disappears with the label. - Fold state is on the control that folds (
aria-expandedon.as-grp,.as-row-chev,.as-sub-chev). - The mobile hamburger in the top bar owns
aria-expandedandaria-controls="as-side-nav", so the off-canvas sidebar is announced as its target. - The colour tile
.sgand the wand are decorative; the label carries the meaning.
Focus
app-shell.cssdeclares 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 fromds.css(outline: 2px solid var(--accent); outline-offset: 1pxon: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-700on the sidebar surface = 12.55:1 light, 11.29:1 dark. - Active row
--ink-900on--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/.amberink 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-currentalone (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.