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.
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()
})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. ↩︎