Angular Material Table with MatSort Accessibility
Permalink to "Angular Material Table with MatSort Accessibility"Angular Material’s mat-table is a thin, flexible table built on the CDK table. Used with native <table mat-table> markup, it renders a semantic HTML table. MatSort and mat-sort-header make headers sortable: the header is keyboard operable and the host th gets aria-sort. The Angular Material documentation explicitly recommends announcing sort changes with LiveAnnouncer, because a screen reader user who activates a sort header otherwise hears little about the result. With a name, an empty row, a labelled paginator and announcements, mat-table is one of the most accessible framework tables available.
This guide belongs to Angular data components.
Spec reference
Permalink to "Spec reference"- Native table markup —
<table mat-table>,<th mat-header-cell>,<td mat-cell>; prefer it over thedisplay: flexelement-based variant for semantics. mat-sort-header— keyboard operable sort control;aria-sorton the header cell;sortActionDescriptionsets the accessible description of the sort action (e.g. “Sort by amount”).LiveAnnouncer(CDK a11y) —announce(message, politeness)writes to a shared live region.*matNoDataRow— a row shown when no data matches; render a single cell spanning all columns.MatPaginator— labelled buttons viaMatPaginatorIntl, which you can translate and customise.
When to use — and when not to
Permalink to "When to use — and when not to"Use mat-table for most Angular data tables: semantic, styleable and composable with sorting, pagination and filtering. Use the native table markup variant.
Do not use the flex-based element variant (<mat-table>) unless you add roles carefully; it relies on ARIA roles to reproduce table semantics. The misapplication: sortable headers without announcements, and an empty table body with no message when filters exclude everything.
Annotated code example
Permalink to "Annotated code example"<h2 id="inv-h">Invoices</h2>
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="announceSort($event)"
aria-labelledby="inv-h"> <!-- name (SC 4.1.2) -->
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by invoice">Invoice</th>
<th mat-cell *matCellDef="let row" scope="row">{{ row.id }}</th> <!-- row header -->
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by amount">Amount (EUR)</th>
<td mat-cell *matCellDef="let row">{{ row.amount | number:'1.2-2' }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="cols"></tr>
<tr mat-row *matRowDef="let row; columns: cols"></tr>
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" [attr.colspan]="cols.length">No invoices match "{{ filter }}".</td> <!-- empty state -->
</tr>
</table>
<mat-paginator [pageSizeOptions]="[25, 50]" aria-label="Invoice pages" (page)="announcePage($event)"></mat-paginator>
constructor(private announcer: LiveAnnouncer) {}
announceSort(s: Sort) {
this.announcer.announce(s.direction ? `Sorted by ${this.label(s.active)}, ${s.direction === 'asc' ? 'ascending' : 'descending'}` : 'Sorting cleared');
}
announcePage(e: PageEvent) {
this.announcer.announce(`Page ${e.pageIndex + 1} of ${Math.ceil(e.length / e.pageSize)}`);
}
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Interaction | Expected | Announcement |
|---|---|---|
| Tab to sort header | Focus on header button | Header, description “Sort by amount” |
| Enter / Space | Sort toggles | aria-sort on th; “Sorted by Amount, ascending” |
| Table navigation | Headers with cells | Native |
| Filter to nothing | No-data row | Row text; announce count separately |
| Paginator next | Next page | Button name; “Page 2 of 10” |
Integration context
Permalink to "Integration context"Sorting in Angular from first principles is covered in building an accessible sortable table in Angular. The CDK’s announcer and focus tools are detailed in Angular CDK a11y: LiveAnnouncer and FocusMonitor. Empty states follow accessible empty state for filtered tables.
Gotchas
Permalink to "Gotchas"sortActionDescription. It is a description, read after the header name; keep it short (“Sort by amount”) and avoid repeating the header text awkwardly.
Row headers. mat-table cells are td by default; use th mat-cell scope="row" for the identifying column so rows have names.
Paginator strings. MatPaginatorIntl provides labels like “Next page”; extend it for translations and for range labels (“1 – 25 of 240”).
Common mistakes
Permalink to "Common mistakes"- Using the element variant without auditing roles.
- No LiveAnnouncer call on sort.
- Empty tables with no matNoDataRow.
- Untranslated paginator strings.
Adoption checklist for teams
Permalink to "Adoption checklist for teams"- Standardise on native table markup.
- Provide a base component or directive that calls LiveAnnouncer on
matSortChange. - Require
sortActionDescriptionon sortable headers (lint or code review). - Include
*matNoDataRowwith a filter-aware message. - Provide a translated
MatPaginatorIntl. - Test with NVDA and VoiceOver after Angular Material upgrades.
Version and upgrade notes
Permalink to "Version and upgrade notes"Angular Material’s MDC-based components (v15+) changed DOM structure and styling; check focus and sort indicator visibility after migrating. sortActionDescription was added to make sort headers more descriptive — use it if your version supports it.
Worked example
Permalink to "Worked example"An insurance portal’s claims table used <mat-table> element markup and MatSort with no announcements. NVDA users heard generic text and could not tell whether sorting worked. The team switched to native table markup, added row headers, sortActionDescription per column, LiveAnnouncer messages for sort and paging, and a matNoDataRow with filter text. The change took a day and made the table behave like a well-built static table with interactive sorting.
Testing it with a screen reader
Permalink to "Testing it with a screen reader"- With NVDA and Chrome, press T to reach the table and confirm its name is announced.
- Use Ctrl+Alt+arrows to move across a row; confirm column headers are read and the row header names the row.
- Tab to a sort header; confirm the header name and the sort description.
- Press Enter; confirm the sort state and the LiveAnnouncer message.
- Filter to zero results; confirm the no-data row is reached and the count is announced.
- Use the paginator; confirm button names and the page announcement.
- Repeat steps 1–4 with VoiceOver and Safari.
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Publish an Angular table kit: native table templates, a sort-announcement directive, a translated paginator intl and empty-row templates. Teams compose tables from the kit rather than from scratch.
FAQ
Permalink to "FAQ"Does MatSort announce sorting?
It sets aria-sort. The Angular Material docs recommend using LiveAnnouncer to announce the change, which you implement.
Should I use mat-table or a grid library?
mat-table for most tables. Use a grid library when you need cell-level keyboard navigation and editing.
How do I make rows have names?
Use th with scope=“row” for the identifying column.
Is the flex variant inaccessible?
Not inherently, but it depends on roles that are easy to break. Prefer native tables.
How do I translate paginator labels?
Provide a custom MatPaginatorIntl with translated strings and range labels.
Can I add filtering?
Yes; label the filter input and announce result counts after filtering.
Does mat-table work with virtual scroll?
With the CDK virtual scroll viewport, yes, with care for row positions; see the CDK virtual scroll guide.
Should the whole header be the sort button?
mat-sort-header makes the header content a button; keep additional controls (menus, filters) separate so each has its own name and action.
Related
Permalink to "Related"- CDK a11y: LiveAnnouncer and FocusMonitor — announcements and focus
- Building an accessible sortable table in Angular — first principles
- PrimeNG table accessibility — an alternative