Vue & Nuxt Data Components

Permalink to "Vue & Nuxt Data Components"

Vue developers building data-heavy apps usually reach for a component library — Vuetify, PrimeVue or Quasar — and, increasingly, Nuxt for routing and rendering. All three libraries render semantic tables by default, which is a good starting point: screen reader users can use their familiar table commands, and headers are announced with cells. The gaps are the same ones every library leaves: names, labels for repeated controls, meaningful empty and error states, announcements when data changes, and route behaviour that makes client-side navigation as clear as a page load.

This topic covers the three libraries and the two pieces of glue that make them work well together: a shared announcer composable and Nuxt route announcements. The users most affected are screen reader users (names, announcements, route changes), keyboard users (sortable headers, focus after navigation), mobile users (card modes) and low-vision users (focus visibility and density). The failure this topic prevents is a Vue app where each table works slightly differently and none of them tells users what just happened.

This topic belongs to accessible data component libraries.

WCAG criteria in scope

Permalink to "WCAG criteria in scope"
Criterion Level Relevance in Vue data apps
1.3.1 Info and Relationships A Table semantics; card structure in grid modes
2.4.2 Page Titled A Titles per Nuxt route
2.4.3 Focus Order A Focus after navigation and on Back
2.4.7 Focus Visible AA Theme focus styles
4.1.2 Name, Role, Value A Table names; checkbox and control labels
4.1.3 Status Messages AA Sorting, filtering, paging, errors, route changes

Prerequisites

Permalink to "Prerequisites"
Vue table libraries compared Matrix of libraries — Vuetify v-data-table, PrimeVue DataTable, Quasar QTable — against semantics, sortable headers, selection labels, empty and loading slots, mobile mode and announcements. Vue table libraries comparedSemanticsSortableheadersSelectionlabelsEmpty andloadingMobile modeAnnouncementsVuetifyTableYesOverride perrowSlotsYour layoutAddPrimeVueTablearia-sortLocale stringsTemplatesResponsivelayoutsAddQuasarTableVerifyOverride perrowSlotsGrid modeAdd
All three render tables; you add names, labels and messages.

ARIA & HTML spec reference

Permalink to "ARIA & HTML spec reference"
Element / attribute Library or you Notes
table, th, td Library Semantic output in all three
aria-sort Library (verify) Add via header slots if missing
Table name (aria-labelledby, caption) You Use pass-through or table attributes
Checkbox labels You (override defaults) Include the record identifier
role="status" / role="alert" regions You One pair at the app root
<title> per route You useHead in Nuxt
tabindex="-1" on h1 You Focus target on navigation

Step-by-step implementation

Permalink to "Step-by-step implementation"

Step 1 — Choose and verify the library

Permalink to "Step 1 — Choose and verify the library"

See Vuetify data table accessibility, PrimeVue DataTable accessibility and Quasar QTable accessibility.

Step 2 — Names and labels (SC 4.1.2)

Permalink to "Step 2 — Names and labels (SC 4.1.2)"
<DataTable :pt="{ table: { 'aria-labelledby': 'inv-h' } }" />
<v-checkbox-btn :aria-label="`Select invoice ${item.id}`" />

Step 3 — One announcer (SC 4.1.3)

Permalink to "Step 3 — One announcer (SC 4.1.3)"
const { announce } = useAnnouncer(); announce('42 invoices shown', { key: 'count' });

See a Vue announcer composable for data updates.

Step 4 — Route announcements (SC 2.4.2, 2.4.3)

Permalink to "Step 4 — Route announcements (SC 2.4.2, 2.4.3)"
<NuxtRouteAnnouncer />

See Nuxt route announcements for data pages.

Step 5 — Structured mobile modes (SC 1.3.1)

Permalink to "Step 5 — Structured mobile modes (SC 1.3.1)"
<article :aria-labelledby="`card-${row.id}`"><h3 :id="`card-${row.id}`">{{ row.id }}</h3><dl>…</dl></article>

Step 6 — Upgrade testing

Permalink to "Step 6 — Upgrade testing"

Run keyboard and screen reader smoke tests and ARIA snapshots on every library upgrade branch.

A Vue data app's accessibility layers Layers: Nuxt routing with titles and focus at the top; pages with query-driven data; table components from a library; the shared announcer; and theme tokens for focus and contrast underneath. A Vue data app's accessibility layersRoutingtitles, route announcer, heading focusPagesquery-driven data; results announcedTable componentslibrary tables with names and labelsAnnouncerroot regions; queue; keysThemefocus, selection, density tokens
Each layer has one owner and one pattern.

Keyboard interaction contract

Permalink to "Keyboard interaction contract"
Context Key Expected Failure indicator
Table header Tab, Enter Sort and announce Mouse-only sorting
Row checkbox Space Toggle; count announced Generic labels
Pagination Tab, Enter Page change announced; focus stays Silent paging
Filter input Typing Results announced after pause Count per keystroke
Link to record Enter Navigate; title announced; focus on h1 Silence
Back Browser Back List restored with focus Top of page

Screen reader compatibility matrix

Permalink to "Screen reader compatibility matrix"
Library / feature NVDA + Firefox JAWS + Chrome VoiceOver + Safari Notes
Vuetify table Good Good Good Name and labels needed
PrimeVue DataTable Good Good Good Translate aria strings
Quasar QTable (table mode) Good Good Good Verify header sorting
Quasar grid mode Depends on cards Depends on cards Depends on cards Headings and dl required
NuxtRouteAnnouncer Announced Announced Announced Titles must be unique

Edge cases & failure modes

Permalink to "Edge cases & failure modes"

1. Attributes not reaching the table

Permalink to "1. Attributes not reaching the table"

Diagnosis: aria-labelledby lands on a wrapper. Fix: pass-through or table-attrs props.

2. Default English strings

Permalink to "2. Default English strings"

Diagnosis: “Select row” in every language. Fix: locale configuration.

3. Regions in components

Permalink to "3. Regions in components"

Diagnosis: silent announcements after re-renders. Fix: root announcer.

4. Focus jumps on filter

Permalink to "4. Focus jumps on filter"

Diagnosis: global afterEach focuses h1. Fix: path-only focus.

5. Unstructured cards

Permalink to "5. Unstructured cards"

Diagnosis: grid mode loses headers. Fix: headings and description lists.

Vuetify data table accessibility

Permalink to "Vuetify data table accessibility"

Semantic tables with slots; add names, per-row labels, meaningful slots and announcements. See Vuetify data table accessibility.

<template #no-data>No invoices match: {{ filterSummary }}.</template>

PrimeVue DataTable accessibility

Permalink to "PrimeVue DataTable accessibility"

Documented accessibility; configure pass-through names, aria locale strings, templates and announcements. See PrimeVue DataTable accessibility.

locale: { aria: { selectAll: 'Select all invoices' } }

Quasar QTable accessibility

Permalink to "Quasar QTable accessibility"

Table and grid modes; verify header sorting, label controls and structure cards. See Quasar QTable accessibility.

<q-th :props="props" :aria-sort="ariaSort(props.col)">{{ props.col.label }}</q-th>

A Vue announcer composable for data updates

Permalink to "A Vue announcer composable for data updates"

One root polite region and one assertive region, a keyed queue and clearing for repeats. See a Vue announcer composable for data updates.

queue.set(key, { message, politeness });

Nuxt route announcements for data pages

Permalink to "Nuxt route announcements for data pages"

Route announcer and heading focus for path changes; data announcer for query changes. See Nuxt route announcements for data pages.

if (to.path === from.path) return; // query-only: no focus move

Cross-cutting concerns

Permalink to "Cross-cutting concerns"

SSR and hydration. Render live regions in the root layout so they exist on first paint, and avoid announcing during hydration; announce only on user-driven changes.

Internationalisation. Library aria strings, your messages and page titles all need translation. Use Vue I18n for your messages and the libraries’ locale options for theirs.

Theming. All three libraries offer theming that can reduce focus visibility. Verify focus and selection contrast in light and dark themes with token tests.

Consistency. If a product uses two libraries (common after acquisitions), give both the same wrapper conventions and announcement wording.

From library default to accessible Vue table Chain: library table renders; you add the name and labels; slots carry meaningful states; the announcer reports results; the route layer handles navigation. From library default to accessible Vue tableLibrary tablesemantic markupNames and labelstable, checkboxes,controlsSlotsloading, empty,errorAnnouncercounts, sort, pagenamesstatesresultsNuxt routingtitles, focus, Back
Each step is small; together they make the difference.

Worked example: a Nuxt admin with three tables

Permalink to "Worked example: a Nuxt admin with three tables"

An admin app built with Nuxt and Vuetify had three tables (users, invoices, audit log), each with its own aria-live div and no names. Navigation between pages was silent. The team added NuxtRouteAnnouncer, unique titles per page and heading focus on path changes; replaced the three live regions with one announcer composable; named each table from its page heading; overrode checkbox labels; and wrote meaningful empty and error slots. A screen reader user who had been relying on a colleague to “check the admin” could now do the weekly user review alone.

Who is affected, and how

Permalink to "Who is affected, and how"
User group Typical failure Fix
Screen reader users Silent navigation and updates Route announcer; data announcer
Keyboard users Focus lost after navigation Heading focus; Back restoration
Mobile users Unstructured cards Headings and labelled values
Low-vision users Theme focus styles Token tests
Non-English users English aria strings Locale configuration

Choosing between Vue libraries

Permalink to "Choosing between Vue libraries"

The three libraries differ less in accessibility than in ecosystem and features. Vuetify fits Material-style apps and has well-structured slots for overriding labels and states. PrimeVue has the broadest feature set and the most explicit accessibility documentation, plus a pass-through API that makes targeting inner elements straightforward. Quasar suits teams building for web, mobile and desktop from one codebase and offers grid mode for small screens. For all three, the deciding questions are practical: can you name the table element, override per-row labels, provide meaningful states and hook into sort, filter and page events? If yes, the library can be made accessible with a wrapper.

Evaluating a library before adoption

Permalink to "Evaluating a library before adoption"

Spend a few hours per candidate: render a table with realistic data, test sortable headers by keyboard, inspect the accessibility tree for names and aria-sort, check checkbox labels, page with a screen reader, switch to mobile layouts, and toggle dark mode. Review the library’s issue tracker for open accessibility issues and response times. Write down what you would need to add in a wrapper; the shorter that list, the better the fit.

Design system integration

Permalink to "Design system integration"

Provide a Vue package with wrappers for your chosen table library, the announcer composable, a Nuxt layer with route announcements and focus handling, and theme tokens. Wrappers should require a table name, supply labels from item-value or dataKey, provide standard slots and wire the announcer. Document which library features are approved and which are discouraged (for example, menu filters on every column).

Common mistakes in Vue data apps

Permalink to "Common mistakes in Vue data apps"
  • Regions in components that re-render or unmount, so announcements are lost.
  • Library defaults left untouched — names, aria strings and empty texts.
  • Global focus on every route change, including query-only changes.
  • Grid or card modes with bare values.
  • No tests after upgrades, despite major API changes between library versions.

Measuring quality

Permalink to "Measuring quality"

For each data page, record: table named (yes/no), labelled controls (yes/no), announcements for sort, filter and page (yes/no), route announcement and focus on navigation (yes/no), and last screen reader test. The resulting checklist across pages shows coverage and makes progress visible.

A short audit script for Vue data pages

Permalink to "A short audit script for Vue data pages"

Open the page with NVDA or VoiceOver. Press T to reach the table and listen for its name. Tab to a sortable header, press Enter, and listen for the sort state and a result message. Tab to a row checkbox and listen for the record identifier. Change a filter and wait for a count. Page forward and listen for the page number. Activate a record link and listen for the page title; press Back and check where focus lands. Ten minutes per page catches nearly every gap described in this topic.

Testing checklist

Permalink to "Testing checklist"

Automated

Permalink to "Automated"

Keyboard

Permalink to "Keyboard"

Screen reader

Permalink to "Screen reader"

FAQ

Permalink to "FAQ"
Which Vue table library is most accessible?

All three render semantic tables. PrimeVue documents accessibility most explicitly; Vuetify and Quasar are solid with configuration. Choose by ecosystem and test.

Do I need a grid instead of a table?

Only for cell-level interaction (spreadsheets, inline editing across cells). Most Vue admin tables work best as semantic tables.

Should I use vue-announcer or my own composable?

Either; the requirements are the same: root regions, queue and clearing.

How do I handle Nuxt pages with heavy data loading?

Announce loading only if it is slow, then announce results; avoid announcing during hydration.

How do I test Vue components for announcements?

Unit test the announcer’s state after component actions; use speech logs for end-to-end checks.

What about Vue 2 apps?

The principles are the same; library APIs differ. Migrate to Vue 3 libraries when possible, as many Vue 2 libraries are no longer maintained.

Should tables have captions?

A caption or a referenced heading gives the table its name. Use whichever fits your layout.

How do I keep three libraries consistent?

Wrappers with the same props, strings and announcement keys.

Does Nuxt server rendering help accessibility?

It helps by delivering real HTML quickly, including table structure, before JavaScript loads. Make sure hydration does not replace focused elements or recreate live regions.

What about Vue Native or mobile frameworks?

For Capacitor or Cordova apps, the web view follows browser accessibility; for native frameworks, use platform accessibility APIs instead of ARIA.

How do we migrate from Vue 2 tables?

Treat the migration as a rebuild: start from a wrapper with the accessibility defaults described here, then port columns and features, and test with screen readers before switching users over.

Who maintains the wrappers?

The design system or platform team, with product teams contributing features. Wrappers without owners decay into copies.

Permalink to "Related"

← Back to Accessible Data Component Libraries