. Semantic HTML helps screen readers and other assistive technologies understand the structure and purpose of our content.
Let’s look at a simple example:
<template>
<div @click="handleClick">Click me!</div>
</template>
<script>
export default {
methods: {
handleClick() {
// Do something
}
}
}
</script>
This might work fine for sighted users, but it’s not great for accessibility. Instead, we should use:
<template>
<button @click="handleClick">Click me!</button>
</template>
<script>
export default {
methods: {
handleClick() {
// Do something
}
}
}
</script>
Now, screen readers will recognize this as a button and users can interact with it using keyboard navigation.
Speaking of keyboard navigation, that’s another crucial aspect of accessibility. Many users rely on keyboards to navigate websites, so we need to make sure our Vue.js apps are fully keyboard accessible. This means adding proper focus management and ensuring all interactive elements can be accessed and activated using only a keyboard.
Here’s a tip: try navigating your app using only the Tab key and Enter. Can you access all the important features? If not, it’s time to make some improvements.
One common issue is custom components that don’t handle keyboard events properly. For example, let’s say we’ve created a custom dropdown component:
<template>
<div class="dropdown">
<div @click="toggle">{{ selectedOption }}</div>
<ul v-if="isOpen">
<li v-for="option in options" :key="option" @click="select(option)">
{{ option }}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
isOpen: false,
selectedOption: 'Select an option',
options: ['Option 1', 'Option 2', 'Option 3']
}
},
methods: {
toggle() {
this.isOpen = !this.isOpen
},
select(option) {
this.selectedOption = option
this.isOpen = false
}
}
}
</script>
This works fine with a mouse, but it’s not keyboard accessible. We can improve it like this:
<template>
<div class="dropdown">
<button @click="toggle" @keydown.esc="close" aria-haspopup="listbox" :aria-expanded="isOpen">
{{ selectedOption }}
</button>
<ul v-if="isOpen" role="listbox">
<li v-for="option in options" :key="option"
@click="select(option)"
@keydown.enter="select(option)"
@keydown.esc="close"
tabindex="0"
role="option"
:aria-selected="option === selectedOption">
{{ option }}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
isOpen: false,
selectedOption: 'Select an option',
options: ['Option 1', 'Option 2', 'Option 3']
}
},
methods: {
toggle() {
this.isOpen = !this.isOpen
},
select(option) {
this.selectedOption = option
this.isOpen = false
},
close() {
this.isOpen = false
}
}
}
</script>
Now our dropdown can be operated with a keyboard and provides appropriate ARIA attributes for screen readers.
Forms are another area where accessibility is super important. We need to make sure our form inputs are properly labeled and that error messages are clear and accessible. Vue.js makes it easy to handle form validation, but we need to make sure we’re communicating errors in an accessible way.
Here’s an example of an accessible form input:
<template>
<div>
<label for="username">Username:</label>
<input id="username" v-model="username" aria-describedby="username-error">
<p id="username-error" v-if="usernameError" role="alert">
{{ usernameError }}
</p>
</div>
</template>
<script>
export default {
data() {
return {
username: '',
usernameError: ''
}
},
watch: {
username() {
this.validateUsername()
}
},
methods: {
validateUsername() {
if (this.username.length < 3) {
this.usernameError = 'Username must be at least 3 characters long'
} else {
this.usernameError = ''
}
}
}
}
</script>
In this example, we’re using aria-describedby
to link the error message to the input, and we’re using role="alert"
to ensure screen readers announce the error message when it appears.
Color contrast is another important consideration for accessibility. We need to make sure there’s enough contrast between text and background colors to ensure readability for all users. There are tools available to check color contrast, and it’s a good idea to run these checks as part of your development process.
One tool that can be super helpful for Vue.js developers is Vue A11y. It’s a collection of Vue.js components and utilities designed to help you build accessible web applications. For example, they have a <VLiveRegion>
component that makes it easy to create ARIA live regions for dynamic content updates:
<template>
<div>
<button @click="updateMessage">Update Message</button>
<VLiveRegion :message="message" />
</div>
</template>
<script>
import { VLiveRegion } from '@vue-a11y/components'
export default {
components: {
VLiveRegion
},
data() {
return {
message: ''
}
},
methods: {
updateMessage() {
this.message = 'This message will be announced by screen readers'
}
}
}
</script>
This ensures that dynamic updates to your app are properly announced to screen reader users.
Another great tool for testing accessibility in Vue.js apps is vue-axe. It’s an accessibility auditing tool that you can integrate directly into your Vue.js development workflow. It runs in the browser and provides real-time feedback on accessibility issues.
Here’s how you can set it up:
import Vue from 'vue'
import VueAxe from 'vue-axe'
if (process.env.NODE_ENV !== 'production') {
Vue.use(VueAxe, {
config: {
rules: [
{ id: 'heading-order', enabled: true },
{ id: 'label-title-only', enabled: true }
]
}
})
}
This will add an accessibility tab to your browser’s developer tools, giving you instant feedback on potential issues.
Now, let’s talk about focus management. When we’re building single-page applications with Vue.js, we need to make sure we’re managing focus correctly as users navigate between different views. This is especially important for screen reader users, as it helps them understand when content has changed.
One way to handle this is by using Vue Router’s navigation guards along with a focus management utility. Here’s an example:
import Vue from 'vue'
import Router from 'vue-router'
import { setFocus } from './focusManagement'
Vue.use(Router)
const router = new Router({
// ... your routes here
})
router.afterEach((to, from) => {
setFocus()
})
export default router
And in your focusManagement.js
file:
export function setFocus() {
const h1 = document.querySelector('h1')
if (h1) {
h1.tabIndex = -1
h1.focus()
}
}
This ensures that focus is set to the main heading of each new page when navigating, providing a clear indication to screen reader users that the content has changed.
It’s also worth mentioning the importance of proper heading structure. Screen reader users often navigate by headings, so having a clear and logical heading structure is crucial. In Vue.js, we can create a reusable heading component that ensures we’re using the correct heading levels:
<template>
<component :is="'h'+level" :class="'heading-'+level">
<slot></slot>
</component>
</template>
<script>
export default {
props: {
level: {
type: Number,
required: true,
validator: value => value >= 1 && value <= 6
}
}
}
</script>
Now we can use it like this:
<template>
<div>
<Heading :level="1">Main Title</Heading>
<Heading :level="2">Subtitle</Heading>
<!-- ... -->
</div>
</template>
<script>
import Heading from './Heading.vue'
export default {
components: {
Heading
}
}
</script>
This ensures we’re always using the correct heading levels and makes it easy to maintain a proper document outline.
Let’s not forget about images. Always provide alternative text for images using the alt
attribute. If an image is purely decorative, use an empty alt
attribute (alt=""
) rather than omitting it entirely. This tells screen readers to skip the image.
<template>
<div>
<img src="logo.png" alt="Company Logo">
<img src="decorative-swirl.png" alt="">
</div>
</template>
Another important aspect of accessibility is ensuring that your app works well with screen magnification. Users with low vision often use screen magnification software to zoom in on parts of the screen. Make sure your layouts don’t break when zoomed and that all content remains accessible.
One way to test this is to use your browser’s zoom feature (usually Ctrl/Cmd + Plus to zoom in). Zoom in to 200% or even 400% and make sure everything still works and looks okay.
When it comes to animations and transitions, which Vue.js makes easy to implement, we need to be mindful of users who are sensitive to motion. The Web Content Accessibility Guidelines (WCAG) recommend providing a way for users to turn off non-essential animations.
We can implement this with a simple Vue mixin:
export const motionSafeMixin = {
data() {
return {
prefersReducedMotion: false
}
},
mounted() {
const query = window.matchMedia('(prefers-reduced-motion: reduce)')
this.prefersReducedMotion = query.matches
query.addListener(this.updateMotionPreference)
},
beforeDestroy() {
window.matchMedia('(prefers-reduced-motion: reduce)').removeListener(this.updateMotionPreference)
},
methods: {
updateMotionPreference(event) {
this.prefersReducedMotion = event.matches
}
}
}
Then in our components:
<template>
<transition :name="prefersReducedMotion ? '' : 'fade'">
<!-- ... -->
</transition>
</template>
<script