How to Build an Accessible Data Table in React
Build an accessible React data table with semantic structure, useful captions, sortable headers, keyboard-friendly actions, and clear loading states.
An accessible React data table must communicate relationships that are visually obvious but otherwise easy to lose. Sighted users can scan columns and infer which header belongs to a value. Screen-reader and keyboard users need those relationships represented in markup and interaction behavior.
The most reliable approach begins with semantic HTML, then adds React behavior without replacing the table's meaning.
Use a real table for tabular data
If information is organized into rows and columns with meaningful relationships, start with <table>, <thead>, <tbody>, <tr>, <th>, and <td>.
Generic <div> elements can be made to resemble a table, but recreating table semantics with ARIA adds complexity and creates more opportunities for mistakes. Native elements already communicate much of the structure assistive technology expects.
Use a table only when the content is actually tabular. A responsive collection of unrelated cards does not become a table simply because its edges align.
Give the table a useful name
A visible caption helps every user understand what the data represents. Use a <caption> when possible:
<table>
<caption>Monthly subscription revenue by plan</caption>
{/* table content */}
</table>
If the design cannot show a visible caption, it may be visually hidden while remaining available to assistive technology. Avoid vague labels such as “Data” or “Results” when several tables appear on the same page.
Mark headers and their scope
Column headers should use <th scope="col">. Row headers should use <th scope="row"> when the first cell identifies the rest of its row.
<thead>
<tr>
<th scope="col">Plan</th>
<th scope="col">Customers</th>
<th scope="col">Revenue</th>
</tr>
</thead>
For tables with multiple header levels, use appropriate colSpan, rowSpan, and header associations. Complex financial or analytical tables deserve testing with the screen readers your audience uses.
Make sorting a real button action
A sortable header should contain a button rather than making the entire header cell behave like one. The button provides expected keyboard focus and activation behavior.
Communicate the current state with aria-sort on the header cell. Valid values include ascending, descending, none, and other.
The button's accessible name should describe the action or result clearly. A visual arrow alone is not enough. When sorting changes, React should update both the data and the semantic state.
Avoid putting every non-interactive cell into the tab order. Keyboard users should navigate to controls, links, and editable fields—not stop at each ordinary value.
Design row actions for keyboard and screen readers
Actions such as edit, duplicate, or delete need specific accessible names. Repeating a button called “Edit” in every row provides little context outside the visual layout.
Include the row identity in the accessible name:
<button aria-label={`Edit ${customer.name}`}>Edit</button>
If clicking a row opens details, provide a real link or button for that action. A click handler on <tr> can be difficult to discover and operate, especially when the row also contains other controls.
Handle selection deliberately
Selectable tables often use a checkbox in each row and a “select all” checkbox in the header. Each checkbox needs an accessible name connected to its row.
The header checkbox should represent mixed selection with its indeterminate state when some, but not all, rows are selected. Announce the number selected near bulk actions so users understand the effect of the controls that appear.
Do not rely on background color alone to identify selected rows. Preserve the checkbox state and consider an additional icon or text cue.
Communicate loading, empty, and error states
Replacing rows during an asynchronous request can leave assistive-technology users unsure whether anything happened.
Use a nearby status message or live region for meaningful changes such as:
- “Loading customer data.”
- “24 results loaded.”
- “No invoices match the current filters.”
- “Customer data could not be loaded.”
Keep the table headers stable where practical. A loading state that removes the entire table can cause users to lose their position and context.
Make filtering and pagination understandable
Connect search and filter controls to the table with clear labels. After filtering, announce the new result count when the change is not otherwise obvious.
Pagination controls should be real buttons or links with names such as “Go to page 3” rather than only the number “3.” Identify the current page with aria-current="page" when page links are used.
Server-side sorting and pagination should preserve the same accessible behavior as client-side operations. The location of the data processing does not change the interaction requirements.
Plan responsive behavior without destroying meaning
Horizontal scrolling is often safer than turning a complex table into unlabeled blocks. If the table scrolls, ensure keyboard focus remains visible and the scroll container itself does not trap navigation.
For smaller datasets, a card transformation can work if every value retains its label. Test zoom and narrow viewport layouts to ensure columns do not overlap or become impossible to reach.
Test the complete interaction
Automated tools can detect missing names and some structural problems, but table usability also requires manual testing.
Verify that you can:
- Identify the caption and headers with a screen reader.
- Reach every interactive control with a keyboard.
- Understand and change the sort direction.
- Select rows and find the resulting bulk actions.
- Filter, paginate, and recover from empty or error states.
- Zoom and use the table on a narrow viewport.
Boreal UI's DataTable documentation demonstrates typed columns, sorting, filtering, pagination, selection, loading states, and accessibility-oriented APIs. Pair it with the broader accessibility guide when building data-heavy interfaces.