Accessibility
Vuetify is committed to providing accessible components that work seamlessly with assistive technologies. This guide covers best practices and built-in accessibility features.
Focus Management
Vuetify provides composables for managing focus states across your application.
Focus Composable
The useFocus composable tracks and manages focus state:
<script setup>
import { useFocus } from 'vuetify'
const { isFocused, focusClasses, focus, blur } = useFocus({
focused: false,
'onUpdate:focused': (value) => console.log('Focus changed:', value),
})
</script>
<template>
<div :class="focusClasses">
<button @focus="focus" @blur="blur">
Focus me
</button>
</div>
</template>
Focus Trap
The useFocusTrap composable keeps keyboard focus within a specific area:
<script setup>
import { ref } from 'vue'
import { useFocusTrap } from 'vuetify'
const isActive = ref(false)
const localTop = ref(true)
const contentEl = ref<HTMLElement>()
useFocusTrap(
{
retainFocus: true,
captureFocus: true,
},
{
isActive,
localTop,
contentEl,
}
)
</script>
<template>
<div ref="contentEl" v-if="isActive">
<input placeholder="First focusable">
<button>Focus stays here</button>
<input placeholder="Last focusable">
</div>
</template>
The focus trap implementation ensures:
- Tab cycles through focusable elements
- Shift+Tab reverses direction
- Focus doesn’t escape the container
- Automatically focuses first element when activated
Use focus traps in modals, dialogs, and dropdown menus to improve keyboard navigation for users who rely on keyboard-only input.
Keyboard Navigation
Vuetify components support standard keyboard interactions:
Buttons
Enter or Space: Activate button
Tab: Move to next focusable element
<template>
<v-btn>Accessible Button</v-btn>
</template>
Buttons automatically render with proper semantics:
role="button" for non-button elements
tabindex="0" for keyboard accessibility
- Event handlers for Enter and Space keys
Form Controls
<template>
<v-text-field
label="Email"
type="email"
hint="Enter your email address"
persistent-hint
/>
</template>
Text fields include:
- Associated
<label> elements
- ARIA attributes for hints and errors
- Keyboard navigation support
- Clear focus indicators
Lists and Menus
<template>
<v-list>
<v-list-item
v-for="item in items"
:key="item.id"
:value="item.id"
@click="selectItem(item)"
>
{{ item.title }}
</v-list-item>
</v-list>
</template>
List navigation:
Arrow Down: Next item
Arrow Up: Previous item
Home: First item
End: Last item
Enter: Activate item
ARIA Support
Vuetify components include appropriate ARIA attributes automatically.
Dynamic ARIA Labels
<template>
<v-btn
icon="mdi-delete"
aria-label="Delete item"
/>
<v-text-field
label="Search"
aria-describedby="search-hint"
/>
<span id="search-hint">Enter keywords to search</span>
</template>
Live Regions
Announce dynamic content changes:
<template>
<div
role="status"
aria-live="polite"
aria-atomic="true"
>
{{ statusMessage }}
</div>
</template>
<script setup>
import { ref } from 'vue'
const statusMessage = ref('')
function updateStatus(message: string) {
statusMessage.value = message
}
</script>
Use aria-live="assertive" sparingly, only for critical alerts that require immediate attention. Most status updates should use "polite".
Form Validation
<template>
<v-form @submit.prevent="handleSubmit">
<v-text-field
v-model="email"
:rules="[rules.required, rules.email]"
:error-messages="errors.email"
aria-required="true"
aria-invalid="!!errors.email"
aria-errormessage="email-error"
/>
<span id="email-error" role="alert">
{{ errors.email }}
</span>
</v-form>
</template>
Screen Reader Support
Optimize content for screen readers:
Visually Hidden Content
Provide context for screen reader users:
<template>
<v-btn icon>
<v-icon>mdi-menu</v-icon>
<span class="sr-only">Open navigation menu</span>
</v-btn>
</template>
<style scoped>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
</style>
Skip Links
Allow users to skip repetitive navigation:
<template>
<div>
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<v-app-bar>
<!-- Navigation -->
</v-app-bar>
<main id="main-content" tabindex="-1">
<!-- Main content -->
</main>
</div>
</template>
<style scoped>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--v-theme-primary);
color: white;
padding: 8px;
text-decoration: none;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
</style>
Color Contrast
Ensure sufficient color contrast for readability.
WCAG Compliance
Vuetify’s default themes meet WCAG 2.1 AA standards:
import { createVuetify } from 'vuetify'
const vuetify = createVuetify({
theme: {
themes: {
light: {
colors: {
primary: '#1867C0', // Contrast ratio: 4.5:1
secondary: '#5CBBF6', // Contrast ratio: 3:1
error: '#B71C1C', // Contrast ratio: 7:1
},
},
},
},
})
Use online contrast checkers like WebAIM’s Contrast Checker to verify your custom theme colors meet WCAG AA (4.5:1) or AAA (7:1) standards.
High Contrast Mode
Support Windows High Contrast Mode:
@media (prefers-contrast: high) {
.v-btn {
border: 2px solid currentColor;
}
}
Reduced Motion
Respect user preferences for reduced motion:
<template>
<v-dialog
v-model="dialog"
:transition="transition"
>
<!-- Dialog content -->
</v-dialog>
</template>
<script setup>
import { computed } from 'vue'
const prefersReducedMotion = computed(() => {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
})
const transition = computed(() => {
return prefersReducedMotion.value ? 'none' : 'dialog-transition'
})
</script>
Or use CSS:
@media (prefers-reduced-motion: reduce) {
.v-btn,
.v-card,
.v-dialog {
transition-duration: 0.001ms !important;
}
}
Semantic HTML
Use appropriate HTML elements for better accessibility:
<template>
<!-- Good: Semantic structure -->
<header>
<v-app-bar>
<nav aria-label="Main navigation">
<!-- Navigation items -->
</nav>
</v-app-bar>
</header>
<main>
<article>
<h1>Article Title</h1>
<p>Content...</p>
</article>
</main>
<footer>
<p>© 2024 Company Name</p>
</footer>
</template>
Landmark Regions
Define page regions for navigation:
<template>
<div>
<header role="banner">
<v-app-bar />
</header>
<nav role="navigation" aria-label="Main">
<v-list />
</nav>
<main role="main">
<v-container />
</main>
<aside role="complementary" aria-label="Sidebar">
<v-navigation-drawer />
</aside>
<footer role="contentinfo">
<v-footer />
</footer>
</div>
</template>
Testing Accessibility
Automated Testing
Use tools to catch common issues:
pnpm add -D @axe-core/vue vitest-axe
import { mount } from '@vue/test-utils'
import { axe } from 'vitest-axe'
import MyComponent from './MyComponent.vue'
test('should not have accessibility violations', async () => {
const wrapper = mount(MyComponent)
const results = await axe(wrapper.element)
expect(results).toHaveNoViolations()
})
Manual Testing
Test with assistive technologies:
- Keyboard Navigation: Navigate using only Tab, Arrow keys, Enter, and Escape
- Screen Readers: Test with NVDA (Windows), JAWS (Windows), or VoiceOver (macOS/iOS)
- Zoom: Test at 200% and 400% zoom levels
- High Contrast: Enable Windows High Contrast mode
- Voice Control: Test with voice control software
Browser Extensions
- axe DevTools: Comprehensive accessibility scanner
- WAVE: Visual feedback about accessibility issues
- Lighthouse: Built into Chrome DevTools
Best Practices
1. Provide Text Alternatives
<template>
<!-- Images -->
<v-img
src="chart.png"
alt="Sales increased 25% in Q4 2024"
/>
<!-- Icons with meaning -->
<v-icon aria-label="Warning">mdi-alert</v-icon>
<!-- Decorative icons -->
<v-icon aria-hidden="true">mdi-arrow-right</v-icon>
</template>
2. Ensure Keyboard Accessibility
<template>
<!-- Don't do this -->
<div @click="handleClick">Click me</div>
<!-- Do this instead -->
<v-btn @click="handleClick">Click me</v-btn>
<!-- Or this for custom elements -->
<div
role="button"
tabindex="0"
@click="handleClick"
@keydown.enter="handleClick"
@keydown.space.prevent="handleClick"
>
Click me
</div>
</template>
3. Form Labels and Instructions
<template>
<v-form>
<v-text-field
v-model="password"
label="Password"
type="password"
:rules="[rules.required, rules.minLength]"
hint="Must be at least 8 characters"
persistent-hint
/>
</v-form>
</template>
4. Error Identification
<template>
<v-alert
v-if="errors.length"
type="error"
role="alert"
>
<strong>Please correct the following errors:</strong>
<ul>
<li v-for="error in errors" :key="error">
{{ error }}
</li>
</ul>
</v-alert>
</template>
5. Meaningful Link Text
<template>
<!-- Don't -->
<a href="/docs">Click here</a> for documentation
<!-- Do -->
<a href="/docs">Read the documentation</a>
</template>
Avoid using “click here”, “read more”, or “learn more” without context. Screen reader users often navigate by links alone.