Accessibility
v0 provides ARIA attributes out-of-the-box through the attrs pattern. You provide styling and visual feedback.
The attrs Pattern
Every v0 component exposes an attrs object containing all accessibility attributes. Spread it onto your elements:
<script setup>
import { Selection } from '@vuetify/v0'
const items = ['Apple', 'Banana', 'Cherry']
</script>
<template>
<Selection.Root v-slot="{ attrs }">
<div v-bind="attrs">
<Selection.Item v-for="item in items" :key="item" v-slot="{ attrs }">
<button v-bind="attrs">{{ item }}</button>
</Selection.Item>
</div>
</Selection.Root>
</template>What’s Included in attrs
| Component | ARIA Attributes Provided |
|---|---|
| Selection.Item | aria-selected, aria-disabled, data-selected, data-disabled |
| Group.Item | role="checkbox", aria-checked, aria-disabled, data-selected, data-disabled, data-mixed |
| ExpansionPanel.Activator | id, role, tabindex, aria-expanded, aria-controls, aria-disabled |
| Pagination.Root | aria-label, role="navigation"[1] |
| Popover.Activator | popovertarget, data-open[2] |
Always spread the attrs object from slot props onto your interactive elements. Missing ARIA attributes break screen reader support.
Developer Responsibilities
v0 provides the ARIA plumbing. You must provide:
| Responsibility | Example |
|---|---|
| Visual focus indicators | :focus-visible { outline: 2px solid blue } |
| Color contrast | Ensure 4.5:1 ratio minimum |
| Visible labels | Add <label> or aria-label for inputs |
| Skip links | Navigation landmarks for keyboard users |
Focus Trapping
v0 does not provide focus trapping. Use external solutions:
Native
inertattribute for siblings
Roving Tabindex
v0 does not provide roving tabindex. This keeps the library headless - implement in your design system layer if needed for arrow key navigation between items.
Teleported Content and Landmarks
Content teleported by Portal renders into body, outside your app’s landmarks, so audit tools flag it with the axe region↗︎ rule. v0 is headless — it won’t pick a landmark role for you. Give the teleported subtree its own semantics: role="dialog" for modals (exempt from the landmark rule), role="status" or role="alert" for toast regions, or role="region" plus aria-label for arbitrary overlays. See the Portal accessibility notes for an example.
Keyboard Navigation
What v0 Handles
tabindexmanagement (-1 when disabled, 0 when enabled)ARIA state synchronization (
aria-expanded,aria-selected)Data attributes for styling (
data-selected,data-disabled)
What You Implement
| Pattern | Keys to Handle |
|---|---|
| List selection | Arrow keys, Home/End |
| Menus | Arrow keys, Escape, Enter |
| Dialogs | Escape to close, focus trap |
| Tabs | Arrow keys, Home/End |
// You implement navigation logic - v0 provides selection state
function onKeydown (e: KeyboardEvent, ids: string[], currentIndex: number) {
switch (e.key) {
case 'ArrowDown': selection.select(ids[currentIndex + 1]); break
case 'ArrowUp': selection.select(ids[currentIndex - 1]); break
case 'Home': selection.select(ids[0]); break
case 'End': selection.select(ids[ids.length - 1]); break
}
}Testing Strategies
Automated Testing
import { axe } from 'vitest-axe'
it('passes accessibility audit', async () => {
const { container } = render(MyComponent)
expect(await axe(container)).toHaveNoViolations()
})vitest-axe is a Node package — it reaches for node:module on import, so it works in a jsdom or happy-dom test environment and not in Vitest’s browser mode. If your components are tested in a real browser, drop the wrapper and call axe-core directly:
import axe from 'axe-core'
it('passes accessibility audit', async () => {
const { container } = render(MyComponent)
const results = await axe.run(container)
expect(results.violations).toEqual([])
})Prefer the browser form where you can. Layout-dependent rules — colour contrast, target size, element overlap — need real computed styles, and a simulated DOM either skips them or answers from a layout that does not exist. v0 audits its own components this way; see packages/0/src/components/a11y.browser.test.ts.
Manual Testing Checklist
Use this checklist during manual QA:
- [ ] Tab through all interactive elements
- [ ] Verify focus visibility
- [ ] Test with keyboard only (no mouse)
- [ ] Check color contrast with DevTools
- [ ] Validate with browser accessibility treeRecommended Tools
| Tool | Purpose |
|---|---|
| axe DevTools | Browser extension for WCAG scanning |
| Lighthouse | Built-in Chrome audit |
| NVDA/VoiceOver | Screen reader verification |
Internationalization
Every v0 component ships an inline English accessible name for the ARIA labels it renders — a Dialog close button reads "Close", a pagination landmark reads "Pagination", and so on. Each default lives at the component’s call site as locale.ti(key) ?? 'English default', so an app that never installs a locale plugin still satisfies WCAG 4.1.2 Name, Role, Value↗︎ out of the box. No locale strings are bundled into the runtime.
To localize those names, install the Locale plugin with translations for the component keys (Dialog.close, Pagination.label, Carousel.next, …). When a key resolves the component uses your translation; when it doesn’t, the inline English default still renders. The optional @vuetify/v0/locale/messages/en export is the canonical map of every key v0 components look up — import it as a starting point for a new translation, or register it as-is for a complete English baseline.
For RTL (right-to-left) support, see useRtl. Direction is managed independently from locale — useRtl provides a reactive isRtl boolean and sets the dir attribute on the target element.
role="navigation"is only added when the root element isn’t already a<nav>. If you renderPagination.Root as="nav", the role is omitted to avoid redundant landmark roles. ↩︎Popover.Activatorwires the native Popover API↗︎ via thepopovertargetattribute. See the Browser Support page for fallback behavior in older browsers. ↩︎