Accessibility
What the table does for you, what it announces, and the four ways a customization can undo it.
Accessibility here is not a mode you turn on. It is the default markup, and the work is in not breaking it.
What you get without asking
- Real table semantics. A
tablewith acaptionor anaria-label,th scope="col"headers and, withfirstColumnAsRowHeader, ath scope="row"per row, so a screen reader can say which record a cell belongs to. - Sort state.
aria-sorton the primary sorted column, which is what the specification allows, and anaria-descriptioncarrying the priority for the others. - Real counts.
aria-rowcountandaria-rowindexreport the whole result, not the rendered window, so a virtualized table says "row 4,201 of 100,000". - Busy state.
aria-busywhile fetching, so a reader is told the table is updating. - Announcements. A polite, debounced live region for sort changes, page changes, result counts, loading, selection counts and column moves: the things a sighted reader sees happen.
- Focus. A visible ring on every interactive element, from the
--tk-focus-ringtoken. - Motion.
prefers-reduced-motiondisables the animations. - Contrast. Every default token pair meets WCAG AA, and that is checked in the test suite rather than asserted here.
The keyboard
Without keyboard navigation, everything is still reachable
by Tab: sort buttons, checkboxes, links, menus. Rows are not focusable unless onRowClick makes
them interactive, which is correct: a row you cannot activate should not be a tab stop.
With it, the table becomes a grid: one tab stop, arrow keys between cells, Enter to step into a cell's controls and Escape to step back out. That is the WAI-ARIA data grid pattern, and it is the right choice for a dense table a person works in all day.
Four ways to break it
- A slot that does not spread its props. The classes, the ARIA attributes and the handlers arrive as props. Dropping them drops the behaviour.
- A slot that changes the element. A
HeaderCellrendered as adivis not a header. - A button inside a button. The default header cell already contains the sort button, so a
custom
HeaderCellmust renderchildrenbeside its addition, not around it. This is the single most common regression. - An icon button with no name.
ActionButtonrequires alabelfor this reason; a bare icon in a custom cell needs one too.
Things worth doing yourself
- Name the table.
aria-labelor acaption. "Table" is not a description, and a page with two tables needs two names. - Localize. Announcements are read aloud; they should be in the reader's language. See Localization.
- Say why it is empty. An empty state that distinguishes "no data" from "no matches" is an accessibility feature as much as a usability one.
- Test it. An axe assertion per configuration you ship. See Testing.
Known exception
The classic preset's header text is #717680 on #FAFAFA, about 4.4:1, just under the AA
threshold for normal text. Those token values are frozen because the preset reproduces an
existing design. Every other preset passes, and the exception is asserted in the test suite so it
cannot spread.