
Build more accessible Vue apps with practical guidance on semantic HTML, ARIA bindings, focus management, accessible routing, keyboard navigation, and common mistakes to avoid.
Reza Baar
March 31, 2026
Accessibility isn't a feature you bolt on at the end. It's a set of decisions you make while building. The good news is that Vue makes most of these decisions pretty straightforward if you know what to reach for.
👇 By the end of this post, you'll know:
This isn't Vue-specific but it's where most accessibility issues come from. So let’s start from here. If you're using <div> and <span> for everything and then adding ARIA roles to make them behave like buttons and links, you're doing it the hard way!
Use the right element:
<button> for actions (please no <div @click>)<a> for navigation<nav>, <main>, <aside>, <header>, <footer> for landmarks<label> linked to form inputs (always)Vue's <template> and fragments don't add extra DOM nodes, so there's no excuse for wrapper-div soup.
Vue's reactive binding system makes ARIA attributes easy to keep in sync with state. Bind them dynamically instead of hardcoding.
<script setup>
import { ref } from 'vue'
const isExpanded = ref(false)
</script>
<template>
<button
:aria-expanded="isExpanded"
@click="isExpanded = !isExpanded"
>
Toggle details
</button>
<div v-show="isExpanded" role="region">
Content here
</div>
</template>
Key ARIA patterns in Vue:
:aria-expanded for disclosure widgets (accordions, dropdowns):aria-selected for tabs and selection UIs:aria-live="polite" on regions that update asynchronously (toast notifications, loading states):aria-disabled alongside :disabled on form elementsrole="alert" for error messages that need immediate announcementNo manual DOM manipulation needed, the reactive system keeps these in sync for free.
This is where Vue apps break accessibility the most. When you conditionally render content with v-if or navigate between views, focus can get lost. A screen reader user ends up at the top of the page with no idea what changed.
<script setup>
import { ref, nextTick } from 'vue'
const showModal = ref(false)
const modalRef = ref(null)
async function openModal() {
showModal.value = true
await nextTick() // these lines are the interesting parts
modalRef.value?.focus()
}
</script>
<template>
<button @click="openModal">Open</button>
<div v-if="showModal" ref="modalRef" tabindex="-1" role="dialog" aria-modal="true">
Modal content
<button @click="showModal = false">Close</button>
</div>
</template>
Why nextTick matters: Vue batches DOM updates. After setting showModal to true, the element doesn't exist in the DOM yet. nextTick waits for the DOM update to complete before calling .focus().
Focus rules to follow:
v-if, make sure focus doesn't get orphanedtabindex="-1" on non-interactive elements that need programmatic focus (like modal containers or heading regions)Single page applications have a fundamental accessibility problem: route changes don't trigger a page load, so screen readers don't announce the new content.
<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const announcement = ref('')
watch(() => route.path, () => {
announcement.value = `Navigated to ${route.meta.title || route.name}`
})
</script>
<template>
<div aria-live="assertive" class="sr-only">
{{ announcement }}
</div>
<RouterView />
</template>
The sr-only class (visually hidden, still readable by screen readers) is something you'll want in your global styles:
.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;
}
After a route change, move focus to the main content area or the page heading. Otherwise screen reader users start from the top of the page every time.
router.afterEach(async () => {
await nextTick()
const heading = document.querySelector('h1')
heading?.focus()
})
Make sure your <h1> has tabindex="-1" so it can receive programmatic focus without appearing in the tab order.
If it works with a mouse, it needs to work with a keyboard. Vue's event modifiers make this cleaner.
<template>
<div
role="listbox"
@keydown.up.prevent="selectPrevious"
@keydown.down.prevent="selectNext"
@keydown.enter="confirmSelection"
@keydown.escape="close"
>
<div
v-for="option in options"
:key="option.id"
role="option"
:aria-selected="option.id === selectedId"
@click="select(option)"
>
{{ option.label }}
</div>
</div>
</template>
Things to check:
Missing labels on inputs: Every <input> needs a <label>. If a visible label doesn't fit the design, use aria-label or aria-labelledby.
<input type="search" aria-label="Search posts" v-model="query" />
Using v-html without caution: Injected HTML skips Vue's template compilation. Any interactive elements inside won't have proper event handlers or ARIA bindings. If you must use v-html, ensure the content is sanitized and accessible.
Forgetting alt on images: If the image is decorative, use alt="" (empty string, not missing). If it conveys information, describe it.
Relying only on color: Error states, status indicators, and required fields need more than just a red border. Add icons, text, or ARIA attributes.
Not testing with a keyboard: Tab through your app regularly. If you can't complete a core flow without a mouse, it's broken for keyboard and screen reader users.
Most accessibility work in Vue isn't about special APIs or libraries. It's about using semantic HTML, keeping ARIA attributes in sync with reactive state, and managing focus when the DOM changes. Vue's reactivity system, template refs, and nextTick give you everything you need. The rest is discipline.
Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.

Accessible Vue Apps
Build more accessible Vue apps with practical guidance on semantic HTML, ARIA bindings, focus management, accessible routing, keyboard navigation, and common mistakes to avoid.
Reza Baar
Mar 31, 2026

Building a Full-Stack app with Nuxt and the Supabase MCP
Advanced Agentic AI & Nuxt: MCP setup, schema design, policies, server routes and the full-stack agentic workflow recap with code examples
Reza Baar
Mar 26, 2026

AI Coding in React: What You Still Need to Know
AI tools like Cursor and Copilot make React development faster, but your React knowledge determines how good the output is. Here's what you need to know.
Aurora Scharff
Mar 24, 2026
We can help you recruit Certified Developers for your organization or project. The team has helped many customers employ suitable resources from a pool of 100s of qualified Developers.
Let us help you get the resources you need.
