React Aria Components Table and GridList
Permalink to "React Aria Components Table and GridList"React Aria Components (RAC), from Adobe, provide unstyled, accessible building blocks. Its Table implements the ARIA grid pattern with sorting, selection, keyboard navigation and screen reader announcements for selection; GridList implements an interactive list where each item can contain actions. Because RAC is headless, you get correct behaviour and full control of appearance — and you are responsible for making that appearance accessible: focus rings, selection states, contrast.
This guide belongs to React data grid libraries.
Spec reference
Permalink to "Spec reference"Table— rendersrole="grid"with rows, column headers and cells; supportsselectionMode(single,multiple),sortDescriptor, keyboard navigation across cells and rows, and typeahead.GridList— rendersrole="grid"with a single column (a list where items can contain interactive elements), supporting selection and actions.- Data attributes —
data-focused,data-focus-visible,data-selected,data-hovered,data-pressedfor styling. - Relevant criteria: SC 2.1.1, SC 4.1.2 (A), SC 2.4.7, SC 1.4.11 (AA) for your styles, SC 4.1.3 (AA) for result announcements.
When to use — and when not to
Permalink to "When to use — and when not to"Use RAC Table when you need an accessible interactive table and want full control of styling — design systems often build their grid on it. Use GridList for record lists where each item is a card with actions.
For very large datasets, combine with RAC’s Virtualizer, and test performance. For simple static tables, a semantic HTML table is still simpler.
The misapplication: styling Table with only :hover states and no [data-focus-visible] styles, so keyboard users cannot see where they are.
Annotated code example
Permalink to "Annotated code example"import { Table, TableHeader, Column, TableBody, Row, Cell, Checkbox } from 'react-aria-components';
function InvoiceTable({ items }) {
const [sort, setSort] = useState({ column: 'due', direction: 'ascending' });
const [selected, setSelected] = useState(new Set());
const sorted = useMemo(() => sortBy(items, sort), [items, sort]);
return (<>
<Table aria-label="Invoices" // SC 4.1.2
selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected}
sortDescriptor={sort} onSortChange={setSort}>
<TableHeader>
<Column><Checkbox slot="selection" aria-label="Select all invoices" /></Column>
<Column id="id" isRowHeader>Invoice</Column> {/* row header for announcements */}
<Column id="customer" allowsSorting>Customer</Column>
<Column id="due" allowsSorting>Due date</Column>
</TableHeader>
<TableBody items={sorted}>
{(item) => (
<Row id={item.id} textValue={`${item.id}, ${item.customer}`}>
<Cell><Checkbox slot="selection" /></Cell>
<Cell>{item.id}</Cell><Cell>{item.customer}</Cell><Cell>{item.due}</Cell>
</Row>
)}
</TableBody>
</Table>
<p role="status" className="visually-hidden">{`${selected === 'all' ? items.length : selected.size} selected`}</p>
</>);
}
/* Your job: visible focus and selection (SC 2.4.7, 1.4.11, 1.4.1) */
.react-aria-Row[data-focus-visible], .react-aria-Cell[data-focus-visible] { outline: 3px solid var(--focus); outline-offset: -3px; }
.react-aria-Row[data-selected] { background: var(--selected-bg); box-shadow: inset 4px 0 0 var(--brand-strong); }
.react-aria-Column[aria-sort="ascending"] .sort-icon { transform: rotate(180deg); }
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Key | Behaviour | Expected announcement | Notes |
|---|---|---|---|
| Tab | Into table | Table name, row and column info | One tab stop |
| Arrows | Row and cell navigation | Row header and cell | Left/Right moves between cells |
| Enter on sortable column | Sort | Column with sort direction | aria-sort provided |
| Space | Toggle row selection | Selection state | Provided |
| Ctrl/Cmd+A | Select all | Selection state | Provided |
| Typing | Typeahead on row textValue | Matched row | Provided |
Integration context
Permalink to "Integration context"RAC tables pair well with TanStack Table for data logic. Row selection behaviour is explained in row selection in TanStack Table React. Low-level hooks for custom grids are covered in React Aria hooks for custom grids.
Gotchas
Permalink to "Gotchas"isRowHeader. Without a row header column, screen readers lack a row name for announcements. Mark the identifying column.
Interactive content in cells. Buttons in cells are supported, but their keyboard reachability follows RAC’s model (Enter or arrow into the cell); test it rather than adding tabIndex manually.
Styling defaults. The starter styles are examples; ensure your production styles keep visible focus in every theme.
Common mistakes
Permalink to "Common mistakes"- No accessible name on Table.
- Missing isRowHeader.
- Styling hover but not focus-visible.
- Expecting the table to announce filter result counts.
Version and upgrade notes
Permalink to "Version and upgrade notes"React Aria Components reached stable releases with frequent minor updates; APIs like Virtualizer, drag-and-drop hooks and tree support have grown over time. Read release notes for behaviour changes in keyboard handling and selection, and keep styles keyed to data attributes rather than internal class structure.
Worked example
Permalink to "Worked example"A design system team rebuilt its table on RAC to replace a custom grid that had 12 open accessibility bugs. The rebuild inherited correct keyboard behaviour, sort semantics and selection announcements; the team’s work was styling focus and selection with tokens tested at 3:1 in both themes, adding isRowHeader, and providing an announcer for filter results. Nine of the twelve bugs disappeared on day one; the remaining three were styling issues fixed with tokens.
Migrating a custom table to React Aria
Permalink to "Migrating a custom table to React Aria"- List every behaviour your current table supports — sorting, selection, row actions, expandable rows, drag — and map each to RAC props or components.
- Choose the row header column and set
isRowHeader. - Move styling from class-based states (
.is-selected) to data attributes ([data-selected]), keeping your tokens. - Replace custom keyboard handlers with RAC’s; delete code rather than layering on top of it.
- Keep your status region and announcer; RAC does not replace them.
- Run your existing keyboard and screen reader tests, then add ARIA snapshots for the new structure.
Teams that delete their old key handlers get the most benefit; teams that keep them often end up with double handling of arrow keys.
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Build the design system’s table on RAC and own the visual layer: tokens for focus, selection and sort icons, and a standard announcer integration. Document which behaviours come from RAC so teams do not reimplement them.
FAQ
Permalink to "FAQ"Is React Aria's Table a grid or a table role?
It renders role=“grid”, because it is interactive. For static tables, use HTML tables.
Do I need to add aria-sort myself?
No. RAC sets aria-sort from sortDescriptor.
Can rows contain buttons?
Yes. Test the keyboard path to them and give them names.
Does it support virtualization?
Yes, via Virtualizer in recent versions.
What does textValue do?
It provides the row’s text for typeahead and for announcements when row content is rich.
Is GridList the same as a listbox?
No. GridList is a grid with one column, allowing interactive content inside items; a listbox cannot contain interactive elements.
Can I use React Aria Table with my own data fetching and sorting?
Yes. RAC is controlled: you pass sorted items and handle onSortChange. Sorting, filtering and pagination logic stays wherever it lives today.
How do I add expandable rows?
Recent versions support tree-like tables; check the docs for your version and test aria-expanded announcements.
Related
Permalink to "Related"- MUI X Data Grid audit — a styled alternative
- React Aria hooks for custom grids — lower-level building blocks
- Listbox versus grid — choosing a list structure