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-sort and 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.
v-data-table: provided versus your job Matrix of concerns — table semantics, sortable headers, selection checkboxes, pagination controls, loading and empty states, announcements, table name — against provided or your job. v-data-table: provided versus your jobProvidedYour jobTable semanticsYesNoSortable headersYesVerify aria-sortSelection checkboxesYesPer-row labelsPagination controlsYesCheck labels and localeLoading and emptySlotsMeaningful textAnnouncementsNoStatus regionTable nameNoCaption or labelledby
Vuetify provides the structure; names and messages come from you.

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.

Default slots versus meaningful states Comparison: default loading and no-data text is generic and silent; meaningful slots name the table, the filters and the next step, and announce through a status region. Default slots versus meaningful states✗ Default text"Loading items…" visually only"No data available"Same message for empty and failedNo count after filtering✓ Meaningful states"Loading invoices" in a status region"No invoices match: Status is Overdue"Separate error slot with retry"Showing 42 invoices"

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
Hardening a Vuetify data table Flow: name the table; label per-row checkboxes; write meaningful loading, no-data and error slots; announce results after each load; verify theme focus and contrast. Hardening a Vuetify data tableNamearia-labelledby or captionfrom the page headingCheckboxesper-row labelsinclude the recordSlotsloading, no-data, errormeaningful textAnnounceafter each loadcount, sort and pageThemefocus and selection3:1 in light and dark

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"
  1. Create a project-level wrapper component around v-data-table (and -server) with a required label or labelledby prop.
  2. In the wrapper, override the selection slots to label checkboxes from item-value.
  3. Provide standard loading, no-data and error slots with translatable text.
  4. Wire update:options and filter changes to the shared announcer.
  5. Set theme tokens for focus and selection and verify contrast in both themes.
  6. 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.

Permalink to "Related"

← Back to Vue & Nuxt Data Components