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 the display: flex element-based variant for semantics.
  • mat-sort-header — keyboard operable sort control; aria-sort on the header cell; sortActionDescription sets 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 via MatPaginatorIntl, which you can translate and customise.
Sorting a mat-table column Sequence among user, sort header, MatSort, LiveAnnouncer and live region: Enter on the Amount header; MatSort emits sortChange; aria-sort updates on the header; the component calls LiveAnnouncer; the live region announces the new order. Sorting a mat-table columnUserSort headerMatSortLiveAnnouncerEnter on "Amount"toggle directionaria-sort="ascending"sortChange event"Sorted by Amount, ascending"
MatSort handles state; you handle the message.

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.

Element variant versus native table variant Comparison: the element-based mat-table depends on ARIA roles applied to generic elements; the native table variant uses real table elements and works naturally with table navigation. Element variant versus native table variant✗ <mat-table> element variantGeneric elements with rolesMore ways to break semanticsFlex layout controlRequires role audits✓ <table mat-table> native variantReal table, th and td elementsSemantics built inTable layout; style with CSSWorks with table commands

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”
What mat-table and MatSort provide Matrix of concerns — semantics, keyboard sorting, aria-sort, sort announcement, table name, empty row, paginator labels — against provided or your job. What mat-table and MatSort provideProvidedYour jobSemantics (native variant)YesNoKeyboard sortingYesNoaria-sortYesNoSort announcementNoLiveAnnouncerTable nameNoaria-labelledbyEmpty rowmatNoDataRowMeaningful textPaginator labelsDefaultsTranslate MatPaginatorIntl
Material provides mechanics; the documentation tells you to add announcements.

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"
  1. Standardise on native table markup.
  2. Provide a base component or directive that calls LiveAnnouncer on matSortChange.
  3. Require sortActionDescription on sortable headers (lint or code review).
  4. Include *matNoDataRow with a filter-aware message.
  5. Provide a translated MatPaginatorIntl.
  6. 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"
  1. With NVDA and Chrome, press T to reach the table and confirm its name is announced.
  2. Use Ctrl+Alt+arrows to move across a row; confirm column headers are read and the row header names the row.
  3. Tab to a sort header; confirm the header name and the sort description.
  4. Press Enter; confirm the sort state and the LiveAnnouncer message.
  5. Filter to zero results; confirm the no-data row is reached and the count is announced.
  6. Use the paginator; confirm button names and the page announcement.
  7. 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.

Permalink to "Related"

← Back to Angular Data Components