Vuetify Data Table Accessibility
Permalink to "Vuetify Data Table Accessibility"Vuetify’s v-data-table (and its server-side sibling v-data-table-server) renders a semantic HTML table with sortable headers, selection checkboxes, pagination and slots for loading and empty states. As a semantic table, it works naturally with screen reader table commands. The work is in the details Vuetify cannot know: what the table is called, what selection checkboxes refer to, what an empty result means, and when to tell users that sorting or paging changed the data.
This guide belongs to Vue & Nuxt data components.
Spec reference
Permalink to "Spec reference"- Semantic table output —
table,thead,th,tbody,td; headers announced in browse mode. - Sortable headers — keyboard operable headers with sort state; verify
aria-sortand that Enter or Space sorts in your version. - SC 4.1.2 (A) — names for the table, checkboxes and footer controls (items-per-page select, page buttons).
- SC 4.1.3 (AA) — announcements after sort, filter and page changes.
- SC 1.4.11, 2.4.7 (AA) — focus and selection visibility in your Vuetify theme.
When to use — and when not to
Permalink to "When to use — and when not to"Use v-data-table for most Vue data tables in Vuetify apps; use v-data-table-server when sorting, filtering and pagination happen on the server, and v-data-table-virtual for large client-side datasets.
Do not rely on Vuetify’s default “No data available” for every empty state. The misapplication: a server table that shows a generic loading bar and “No data available” while a request is still in flight — screen reader users hear nothing, then “No data” when there actually is data.
Annotated code example
Permalink to "Annotated code example"<template>
<h2 id="inv-h">Invoices</h2>
<v-data-table-server
v-model="selected"
v-model:sort-by="sortBy"
:headers="headers" :items="items" :items-length="total" :loading="loading"
item-value="id" show-select
aria-labelledby="inv-h" <!-- name (SC 4.1.2); verify it reaches the table -->
@update:options="load"
>
<template #loading><span>Loading invoices…</span></template>
<template #no-data>
<p>No invoices match: {{ filterSummary }}. <v-btn variant="text" @click="clearFilters">Clear filters</v-btn></p>
</template>
<!-- Per-row checkbox label (SC 4.1.2) -->
<template #item.data-table-select="{ item, internalItem, isSelected, toggleSelect }">
<v-checkbox-btn :model-value="isSelected(internalItem)" @update:model-value="toggleSelect(internalItem)"
:aria-label="`Select invoice ${item.id}`" />
</template>
</v-data-table-server>
<p role="status" class="visually-hidden">{{ status }}</p> <!-- SC 4.1.3 -->
</template>
<script setup>
const status = ref('');
async function load({ page, itemsPerPage, sortBy }) {
loading.value = true; status.value = 'Loading invoices';
const res = await fetchInvoices({ page, itemsPerPage, sortBy, filters: filters.value });
items.value = res.items; total.value = res.total; loading.value = false;
const s = sortBy[0];
status.value = `${res.total} invoices${s ? `, sorted by ${label(s.key)} ${s.order === 'asc' ? 'ascending' : 'descending'}` : ''}, page ${page}`;
}
</script>
Slot names and props follow Vuetify 3’s data table API; check them against your version.
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Interaction | Expected behaviour | Expected announcement |
|---|---|---|
| T (table navigation) | Reaches the table | “Invoices, table, 26 rows, 5 columns” |
| Tab to sortable header | Focus on header | Header name and sort state |
| Enter / Space on header | Sort | Sort state and status message |
| Tab to row checkbox | Focus | “Select invoice INV-1042, checkbox, not checked” |
| Items-per-page select | Change page size | Select label and value |
| Next page | Load next page | “Page 2” via status |
Integration context
Permalink to "Integration context"Pagination announcements follow announcing page changes in paginated tables and Vue-specific pagination in accessible pagination component in Vue. Sorting in Vue is covered in building an accessible sortable table in Vue. A reusable announcer is in a Vue announcer composable for data updates.
Gotchas
Permalink to "Gotchas"aria-label forwarding. Attributes on v-data-table may land on a wrapper rather than the table. Inspect the DOM; if needed, use the top slot to render a caption-like heading and reference it, or set attributes on the table element after mount.
Expandable rows. show-expand adds expand buttons; check they have names (“Show details for INV-1042”) and aria-expanded.
Virtual tables. v-data-table-virtual renders a subset of rows; screen readers see only those, so consider pagination for screen reader-heavy workflows.
Common mistakes
Permalink to "Common mistakes"- No table name.
- Generic checkbox labels.
- “No data available” shown during loading or after errors.
- Silent server-side updates.
Version and upgrade notes
Permalink to "Version and upgrade notes"Vuetify 3 rewrote the data table components compared with Vuetify 2; slot names, props and markup changed. When migrating, re-verify sortable header keyboard behaviour, checkbox labels and the location of forwarded attributes. Pin minor versions in design system packages and re-test after upgrades.
Worked example
Permalink to "Worked example"A logistics app’s shipment table used v-data-table-server with defaults. Screen reader users heard “table” with no name, “checkbox” fifty times with no record, and nothing when they paged or sorted. The team referenced the page heading, labelled checkboxes with shipment IDs, replaced “No data available” with filter-specific text and a Clear filters button, and announced counts and pages after each load. They also added an error slot so failed requests no longer looked like empty results.
Adoption checklist for teams
Permalink to "Adoption checklist for teams"- Create a project-level wrapper component around
v-data-table(and-server) with a requiredlabelorlabelledbyprop. - In the wrapper, override the selection slots to label checkboxes from
item-value. - Provide standard
loading,no-dataand error slots with translatable text. - Wire
update:optionsand filter changes to the shared announcer. - Set theme tokens for focus and selection and verify contrast in both themes.
- Add an ARIA snapshot test for the wrapper’s default, sorted and empty states.
With the wrapper in place, product teams write only columns and data; the accessibility details come with it.
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Wrap v-data-table in a project component that requires a heading or label, supplies per-row checkbox labels from item-value, provides standard loading, empty and error slots and wires the shared announcer.
FAQ
Permalink to "FAQ"Is v-data-table a grid or a table?
A semantic table. Users navigate it with screen reader table commands.
Does Vuetify announce sorting?
Not as a status message. Add one after sorting completes.
How do I label the select-all checkbox?
Override the header select slot and give it an aria-label like “Select all invoices on this page”.
Should I use the virtual table?
For very large client-side data, yes, but test screen reader access to rows; paginated tables are often easier to use non-visually.
How do I handle errors in server tables?
Track error state separately and render an error message with a retry, rather than falling back to no-data.
Can I add keyboard row navigation?
Semantic tables rely on browse mode. If you need cell focus and arrow keys, consider a grid component instead.
How do expandable rows announce their state?
Check that the expand button has a name with the record (“Show details for INV-1042”) and aria-expanded that updates. Override the expand slot if needed.
Related
Permalink to "Related"- PrimeVue DataTable accessibility — an alternative Vue table
- Vue announcer composable — shared announcements
- Accessible pagination component in Vue — pagination details