12/18/2025

Accessibility in Nuxt: What It Is, How It Works, and How to Optimize

Accessibility in Nuxt: What It Is, How It Works, and How to Optimize

Web accessibility (abbreviated as a11y) is the practice of creating websites that everyone can use. People with visual, auditory, motor, or cognitive impairments should have the same experience as other users.

In this guide, we'll explore how to implement accessibility in Nuxt applications: from basic principles to specific tools.

What is Web Accessibility?

Accessibility isn't just about people with disabilities. It also covers:

  • Users with temporary limitations (a broken arm, bright sunlight)
  • Elderly people with deteriorating vision
  • Users with slow internet connections or old devices
  • People who only use a keyboard

Fact: According to WebAIM, among the top one million websites, Vue.js applications have traditionally had the worst accessibility scores among JavaScript frameworks. This is what prompted the community to create the Vue A11y project.

Why It's Important

ReasonDescription
EthicalEveryone deserves equal access to information
LegalMany countries have accessibility laws (ADA, EAA, Section 508)
Business15% of the world's population has some form of disability
SEOSearch engines better index accessible sites

The WCAG Standard: Levels and Principles

WCAG (Web Content Accessibility Guidelines) is the international accessibility standard from the W3C. The current version is WCAG 2.2.

The Four Principles (POUR)

  1. Perceivable — Content must be presented in a way that can be perceived.
  2. Operable — The interface must be operable.
  3. Understandable — Information must be understandable.
  4. Robust — Content must work with various technologies.

Levels of Conformance

LevelDescriptionRequirements
AMinimalBasic barriers removed
AARecommendedThe standard for most laws
AAAHighestNot always achievable for all content

Recommendation: Aim for Level AA—it's required by most laws and covers the essential needs of users.

Key WCAG 2.2 AA Requirements

  • Text contrast: minimum 4.5:1 for normal text, 3:1 for large text
  • Keyboard accessibility for all functions
  • Visible focus on interactive elements
  • Alternative text for images
  • Ability to stop automatic content

Why Nuxt is Well-Suited for Accessibility

Nuxt has several built-in mechanisms that make it easier to create accessible applications.

Server-Side Rendering

SSR provides a complete markup on the initial load. Screen readers get all the content without waiting for JavaScript.

Route Announcer

Nuxt 3 automatically announces route changes for assistive technologies:

<!-- app.vue -->
<template>
  <NuxtRouteAnnouncer />
  <NuxtPage />
</template>

The component creates a hidden element with aria-live="polite" that informs screen readers about the new page.

Automatic Title

During navigation, Nuxt updates document.title, which is also announced by screen readers.

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      titleTemplate: '%s | My Site'
    }
  }
})

Semantic Markup and ARIA

Semantic HTML is the foundation of accessibility. Use the correct elements instead of <div> and <span>.

Proper Heading Structure

<template>
  <main>
    <h1>Main Page Heading</h1>
    
    <section>
      <h2>Section</h2>
      <h3>Subsection</h3>
    </section>
    
    <aside>
      <h2>Sidebar</h2>
    </aside>
  </main>
</template>

Important: Do not skip heading levels. An <h2> should follow an <h1>, not an <h3>.

Landmarks

Screen readers use landmarks for quick navigation:

<template>
  <header role="banner">
    <nav aria-label="Main Navigation">
      <!-- menu -->
    </nav>
  </header>
  
  <main role="main">
    <!-- main content -->
  </main>
  
  <aside role="complementary">
    <!-- supplementary content -->
  </aside>
  
  <footer role="contentinfo">
    <!-- footer -->
  </footer>
</template>

ARIA Attributes

ARIA (Accessible Rich Internet Applications) supplements semantics where HTML is insufficient:

<template>
  <!-- Toggle button -->
  <button 
    :aria-expanded="isOpen"
    :aria-controls="panelId"
    @click="toggle"
  >
    Show Details
  </button>
  
  <div 
    :id="panelId"
    v-show="isOpen"
    role="region"
    aria-labelledby="toggle-btn"
  >
    Panel content
  </div>
</template>

Key ARIA attributes:

AttributePurpose
aria-labelText label for an element
aria-labelledbyReference to an element with a label
aria-describedbyReference to a description of the element
aria-expandedState of an expanded/collapsed element
aria-hiddenHides an element from assistive technologies
aria-liveAnnounces dynamic changes

Rule: Do not use ARIA to "fix" incorrect HTML markup. Fix the semantics first.

Keyboard Navigation

Every interactive element must be accessible via the keyboard.

Basic Keys

KeyAction
TabMove to the next element
Shift + TabMove to the previous element
Enter / SpaceActivate the element
EscapeClose modals, menus
ArrowsNavigate within components

Focus Management

<script setup lang="ts">
const headingRef = ref<HTMLElement | null>(null)

// Focus on the heading after navigation
onMounted(() => {
  headingRef.value?.focus()
})
</script>

<template>
  <h1 ref="headingRef" tabindex="-1">
    Page Title
  </h1>
</template>

tabindex="-1" allows programmatic focus on an element but excludes it from the natural tab order.

Allows users to bypass repetitive navigation:

<template>
  <a 
    href="#main-content" 
    class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-white focus:p-4"
  >
    Skip to main content
  </a>
  
  <header>
    <!-- navigation -->
  </header>
  
  <main id="main-content">
    <!-- content -->
  </main>
</template>

Focus Trap for Modals

<script setup lang="ts">
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'

const modalRef = ref<HTMLElement | null>(null)
const { activate, deactivate } = useFocusTrap(modalRef)

watch(() => props.open, (isOpen) => {
  if (isOpen) {
    activate()
  } else {
    deactivate()
  }
})
</script>

<template>
  <div 
    v-if="open"
    ref="modalRef"
    role="dialog"
    aria-modal="true"
    aria-labelledby="modal-title"
  >
    <h2 id="modal-title">Modal Title</h2>
    <!-- content -->
    <button @click="close">Close</button>
  </div>
</template>

Announcing Changes for Screen Readers

SPAs have a problem: on navigation, the page doesn't reload, and the screen reader doesn't know that something has changed.

Built-in Route Announcer

Nuxt 3 solves this automatically:

<!-- app.vue -->
<template>
  <NuxtRouteAnnouncer />
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

The component announces the new page's title with aria-live="polite".

Custom Announcements

For dynamic changes (adding to cart, form errors), use live regions:

<script setup lang="ts">
const announcement = ref('')

function addToCart(product: string) {
  // business logic
  announcement.value = `${product} added to cart`
  
  // clear after 5 seconds
  setTimeout(() => {
    announcement.value = ''
  }, 5000)
}
</script>

<template>
  <!-- Hidden container for announcements -->
  <div 
    aria-live="polite" 
    aria-atomic="true"
    class="sr-only"
  >
    {{ announcement }}
  </div>
  
  <button @click="addToCart('Product')">
    Add to Cart
  </button>
</template>

aria-live Modes

ValueBehavior
politeWaits for the current announcement to finish
assertiveInterrupts the current announcement (for errors)
offDisabled

Tip: Use assertive only for critical messages. Frequent interruptions can be annoying to users.

Contrast and Colors

WCAG Contrast Requirements

Text SizeLevel AALevel AAA
Normal (< 18px)4.5:17:1
Large (≥ 18px or ≥ 14px bold)3:14.5:1
UI Components3:1

Checking Contrast

/* Example of sufficient contrast */
.good-contrast {
  color: #1a1a1a;        /* dark text */
  background: #ffffff;   /* white background */
  /* Contrast: 16.1:1 ✓ */
}

/* Insufficient contrast */
.bad-contrast {
  color: #767676;        /* gray text */
  background: #ffffff;   /* white background */
  /* Contrast: 4.48:1 — barely passes AA */
}

Don't Rely Only on Color

<template>
  <!-- Bad: only color indicates an error -->
  <input :class="{ 'border-red-500': hasError }">
  
  <!-- Good: color + icon + text -->
  <div>
    <input 
      :class="{ 'border-red-500': hasError }"
      :aria-invalid="hasError"
      aria-describedby="error-message"
    >
    <p v-if="hasError" id="error-message" class="text-red-500">
      <span aria-hidden="true">⚠️</span>
      Enter a valid email
    </p>
  </div>
</template>

Accessible UI Libraries for Nuxt

Nuxt UI

The official library with built-in accessibility, built on Reka UI:

npx nuxi module add @nuxt/ui

What's included:

  • Automatic ARIA attributes
  • Keyboard navigation
  • Focus management
  • Screen reader support
<template>
  <!-- Accessible dialog out of the box -->
  <UModal v-model="isOpen">
    <UCard>
      <template #header>
        Confirmation
      </template>
      
      <p>Are you sure?</p>
      
      <template #footer>
        <UButton @click="confirm">Yes</UButton>
        <UButton variant="ghost" @click="isOpen = false">
          Cancel
        </UButton>
      </template>
    </UCard>
  </UModal>
</template>

Reka UI (Radix Vue)

Unstyled primitives with full accessibility:

npm install reka-ui
<script setup lang="ts">
import { AccordionRoot, AccordionItem, AccordionTrigger, AccordionContent } from 'reka-ui'
</script>

<template>
  <AccordionRoot type="single" collapsible>
    <AccordionItem value="item-1">
      <AccordionTrigger>Question 1</AccordionTrigger>
      <AccordionContent>Answer 1</AccordionContent>
    </AccordionItem>
  </AccordionRoot>
</template>

shadcn-vue

Copy-paste components based on Radix Vue:

npx shadcn-vue@radix init
npx shadcn-vue@radix add button dialog

Testing Tools

ESLint Plugin

npm install -D eslint-plugin-vuejs-accessibility
// eslint.config.js (ESLint 9+)
import pluginVueA11y from 'eslint-plugin-vuejs-accessibility'

export default [
  ...pluginVueA11y.configs['flat/recommended'],
  {
    rules: {
      'vuejs-accessibility/alt-text': 'error',
      'vuejs-accessibility/anchor-has-content': 'error',
      'vuejs-accessibility/click-events-have-key-events': 'error',
      'vuejs-accessibility/label-has-for': 'error'
    }
  }
]

nuxt-a11y module

Integrates axe-core into DevTools:

npm install -D nuxt-a11y
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-a11y']
})

The module shows accessibility violations in the browser console.

Manual Testing

ToolPurpose
axe DevToolsBrowser extension for auditing
WAVEVisual audit of the page
LighthouseBuilt into Chrome DevTools
NVDA / VoiceOverReal screen readers
KeyboardNavigation without a mouse

Testing with a Screen Reader

  1. VoiceOver (macOS): Cmd + F5
  2. NVDA (Windows): a free screen reader
  3. TalkBack (Android): built-in

Recommendation: Regularly test with a real screen reader. Automated tools find only ~30% of problems.

Accessibility Checklist

Semantics and Structure

  • Semantic HTML elements are used
  • Headings are in sequential order (h1 → h2 → h3)
  • Landmarks are present: header, main, nav, footer
  • Page language is specified (<html lang="en">)

Images and Media

  • All informative images have alt text
  • Decorative images have alt=""
  • Videos have captions
  • Audio has a text transcript

Forms

  • Every input has an associated label
  • Errors are described with text, not just color
  • Required fields are marked (aria-required)
  • Groups of fields are enclosed in a fieldset
  • All functionality is accessible from the keyboard
  • Focus is visible on all elements
  • A skip link to the main content is present
  • Route Announcer is enabled

Colors

  • Text contrast is ≥ 4.5:1
  • UI component contrast is ≥ 3:1
  • Information is conveyed by more than just color

Interactive Elements

  • Buttons have accessible names
  • Links are descriptive (not "click here")
  • Modals have a focus trap
  • Dynamic changes are announced

Sources