Data Table

How the Data Table works with a keyboard and assistive tech, and where it stands against WCAG 2.2 AA.

FreeReact and Vue
AAWCAG 2.2 target
14/15contrast pairs passing
4keyboard interactions

The Data Table uses ARIA table roles on divs (role="table", "row", "columnheader", "cell"), so every cell is read with its column. Its controls are native: sort buttons in the headers, checkbox buttons in the rows, a radio group, search field and buttons in the toolbar. The grid sits in a focusable, labelled scroll region so it can be scrolled from the keyboard.

Keyboard#

Tab

Moves through the toolbar, then the scroll region, then each header checkbox and sort button, then each row's checkbox and More button, in reading order.

←+→

In the All / Active / Inactive tabs: moves and selects. On the focused scroll region: scrolls the table sideways.

Enter

On a sort button: sorts ascending, then descending, then not at all. On More: opens its menu.

Space

On a checkbox: selects or clears the row. On select-all: selects or clears every visible row.

Semantics#

role="table" aria-label

Named from ariaLabel, such as "Contacts". A native <table> would do the same with less to go wrong; prefer it in production.

Always
role="region" tabindex="0"

The scroll box, named "{label}, scrollable", so keyboard users can scroll a wide table.

Always
role="columnheader" aria-sort

"ascending" or "descending" on the sorted column, "none" on the others. The sort control inside is a <button>.

Sortable column
role="checkbox" aria-checked

Buttons with checkbox semantics. Each row's is named "Select {name}"; select-all reports "mixed" when only some rows are chosen.

Row and select-all
aria-selected

Set on the row, but not reliably read inside a plain table. The checkbox's state is what is announced.

Selected row
role="status"

The empty message is announced when a search leaves nothing.

No results

Colour contrast#

Every variant and state has to meet WCAG 2.2 Level AA. Measure the colours as they land on screen: a translucent hover or press wash over whatever sits behind it.

4.5:1

Cell and header text · Default, Hover, Selected

Titles, descriptions and header labels against the row, its 4% hover wash, the #ebf2ff selected tint and the 4% header band, all over white.

WCAG 1.4.3 Contrast (Minimum) ↗
3:1

Checkbox and status icon · Controls, Status

The unchecked box is only a 1px #e1e1e1 stroke (1.31:1 on white), under 3:1. The status check is #229a14 on white (3.68:1).

WCAG 1.4.11 Non-text Contrast ↗
3:1

Focus indicator · Focus visible

Rings in #1a6ef8 (4.54:1 on white): inset on sort, More and tab buttons, outside on checkboxes and the scroll region.

WCAG 1.4.11 Non-text Contrast ↗WCAG 2.4.13 Focus Appearance ↗
Structure

Relationships · Always

Headers are announced with their cells only because every cell sits in a row under a columnheader. Don't wrap rows or cells in extra roles.

WCAG 1.3.1 Info and Relationships ↗
Exception

Reflow · Narrow screens

Data tables are allowed to scroll in two dimensions. The scroll bar is visible, and the region is focusable.

WCAG 1.4.10 Reflow ↗

Measured contrast#

Text against its background, over white, from the table's tokens. For the header, Selected is the sorted column; for the toolbar tabs, the chosen tab. 1 of 15 pairs fall below their minimum (4.5:1 for text, 3:1 for graphics) and are marked in amber.

Text · whereDefaultHoverSelected
Title · Row14.35passes13.17passes12.77passes
Description, passive title · Row5.17passes4.75passes4.60passes
Column label · Header4.75passes13.17passes13.17passes
Filter tab · Toolbar2.89below the minimum12.82passes14.35passes
Filter, Settings · Toolbar5.17passes13.17passes13.17passes

Motion#

  • The row hover wash and checkbox fill change over 120–150ms and are turned off under prefers-reduced-motion.

Checklist#

Give every table an ariaLabel, or a visible caption it points to. "Table, scrollable" is the fallback.

Label each row checkbox with something that identifies the row ("Select Noah Reed"), and each More button the same way.

Unselected toolbar tabs are #8f8f8f on #f2f2f2, about 2.9:1, and the search placeholder is #8f8f8f on white, about 3.2:1. Both need a darker shade.

The empty checkbox's stroke is about 1.3:1. Darken it to #8f8f8f or similar so the control can be seen.

The status badge shows the same green check for Active and Inactive. The word carries the meaning; give Inactive a muted icon so the icon doesn't contradict it.

Use a native <table> with <th scope="col"> when the code library ships; the roles here are a stand-in.