Skip to main content

Accessibility

ListTable generates accessible, semantic HTML tables that work well with screen readers and other assistive technologies.

Semantic HTML

The component uses proper semantic HTML elements without ARIA attributes (which aren't needed for standard tables):

<table>
<caption>Accessible table description</caption>
<thead>
<tr>
<th scope="col">Column Header</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Row Header</th>
<td>Data Cell</td>
</tr>
</tbody>
</table>

Table captions

Always provide a caption property to give context to screen reader users:

<ListTable caption="Quarterly sales report">
- - Q1
- Q2
- Q3
- - $100k
- $120k
- $140k
</ListTable>

Screen readers announce: "Quarterly sales report, table with 2 rows and 3 columns"

Header scope

The component automatically assigns correct scope attributes.

Column headers

Use headerRows to create column headers with scope="col":

<ListTable headerRows={1}>
- - Product
- Price
- - Widget
- $10
</ListTable>

Generates:

<thead>
<tr>
<th scope="col">Product</th>
<th scope="col">Price</th>
</tr>
</thead>

Row headers

Use headerColumns to create row headers with scope="row":

<ListTable headerColumns={1}>
- - Widget
- $10
- - Gadget
- $20
</ListTable>

Generates:

<tbody>
<tr>
<th scope="row">Widget</th>
<td>$10</td>
</tr>
</tbody>

Both row and column headers

Combine both for complex tables:

<ListTable headerRows={1} headerColumns={1}>
- - Product
- Q1
- Q2
- - Widget
- $10
- $12
- - Gadget
- $20
- $25
</ListTable>

The top-left cell becomes a header for both row and column headers.

Merged cells

Screen readers correctly announce merged cells with rowspan/colspan:

<ListTable headerRows={1}>
- - Month
- [c2] Revenue
- _
- - January
- Sales
- Services
</ListTable>

Screen reader announcement: "Revenue, column header, spans 2 columns"

Testing with screen readers

The component has been tested with:

NVDA (Windows)

  • Tables navigated correctly with Ctrl+Alt+Arrow keys
  • Headers announced properly
  • Merged cells span info provided
  • Caption read on table entry

JAWS (Windows)

  • Table navigation works as expected
  • Row/column context maintained
  • Proper header association

VoiceOver (macOS)

  • VO+Arrow keys navigate correctly
  • Headers announced with cells
  • Table summary provided

VoiceOver (iOS)

  • Swipe gestures work properly
  • Headers read with content
  • Table context provided

Keyboard navigation

Standard table keyboard navigation works:

  • Tab - Move between interactive elements
  • Arrow keys - Navigate cells (screen reader specific)
  • Home/End - Jump to row/table boundaries (screen reader specific)

WCAG compliance

The component follows WCAG 2.1 Level AA guidelines:

1.3.1 info and relationships (level A)

  • Semantic markup conveys structure
  • Table relationships programmatically determined
  • Headers properly associated with data cells

1.3.2 meaningful sequence (level A)

  • Reading order follows visual layout
  • Nested lists parse to logical table structure

4.1.1 parsing (level A)

  • Valid HTML table structure
  • No duplicate IDs
  • Proper nesting of elements

4.1.2 name, role, value (level A)

  • Native HTML semantics (no custom roles needed)
  • Table, row, cell roles implicit
  • Headers have accessible names

Color and contrast

The component renders unstyled tables. When adding CSS:

Text contrast

Ensure text meets minimum contrast ratios:

  • Normal text: 4.5:1 minimum
  • Large text (18pt+): 3:1 minimum

Focus indicators

Provide visible focus styles for keyboard users:

.my-table td:focus,
.my-table th:focus {
outline: 2px solid #005fcc;
outline-offset: 2px;
}

Don't rely on color alone

Use additional visual cues beyond color:

<!-- Good: Icon + color -->
<ListTable>
- - Status
- Value
- - ✓ Success
- 100%
- - ✗ Failed
- 0%
</ListTable>

<!-- Bad: color only (not in MDX) -->

Best practices

Do

  • Always provide a caption for context
  • Use headerRows for column headers
  • Use headerColumns for row headers
  • Keep tables simple when possible
  • Test with actual screen readers

Don't

  • Don't use tables for layout (use CSS Grid/Flexbox)
  • Don't nest tables unnecessarily
  • Don't omit headers for data tables
  • Don't use empty header cells (fails axe audit)
  • Don't rely on visual formatting alone

Automated testing

The component passes axe-core accessibility audits:

import { axe } from 'jest-axe';

test('table has no accessibility violations', async () => {
const { container } = render(<ListTable {...props} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

All built-in examples pass axe-core with zero violations.

Complex tables

For very complex tables with multiple header levels:

Use headerRows for stacked headers

<ListTable headerRows={2}>
- - Product
- [c2] Q1
- _
- - _
- Jan
- Feb
- - Widget
- 10
- 12
</ListTable>

Screen readers navigate this correctly with proper scope associations.

Consider simplifying

If a table has more than 2 levels of headers, consider:

  • Splitting into multiple simpler tables
  • Using a different visualization (chart, list)
  • Providing a text summary alongside the table

Screen reader specific features

Table summary

Screen readers announce table dimensions:

"Table with 5 rows and 3 columns"

Screen readers can jump between headers:

  • T key (NVDA/JAWS) - Jump to next table
  • Ctrl+Alt+Arrow (NVDA) - Navigate table cells
  • VO+Arrow (VoiceOver) - Navigate table cells

Reading strategy

Screen readers offer multiple reading modes:

  • Read all content sequentially
  • Navigate cell by cell
  • List all headers
  • Jump to specific row/column

Common accessibility issues

Empty headers

Don't leave header cells empty:

<!-- Bad -->
<ListTable headerRows={1} headerColumns={1}>
- - ← Empty header
- Column 1
- - Row 1
- Data
</ListTable>

<!-- Good -->
<ListTable headerRows={1} headerColumns={1}>
- - Category
- Column 1
- - Row 1
- Data
</ListTable>

Empty headers fail axe-core audits.

Missing caption

Provide context with captions:

<!-- Bad: No context -->
<ListTable>
- - Q1
- Q2
- - $100k
- $120k
</ListTable>

<!-- Good: Clear context -->
<ListTable caption="Quarterly revenue (in thousands)">
- - Q1
- Q2
- - $100k
- $120k
</ListTable>

Layout tables

Don't use ListTable for layout. Use CSS instead.

Further reading