Fokus, ARIA und Keyboard-Support richtig umsetzen
Barrierefreiheit in Vue-Applikationen ist keine optionale Zusatzanforderung – sie ist technische Qualität. Wer Fokusmanagement, ARIA-Attribute und Keyboard-Navigation von Anfang an in Vue-Komponenten einbaut, baut Interfaces, die für alle Nutzenden zuverlässig funktionieren und gleichzeitig rechtliche Anforderungen wie den European Accessibility Act erfüllen.
Inhaltsverzeichnis
- 1. Warum Accessibility in Vue-Komponenten entscheidend ist
- 2. Semantisches HTML als Fundament
- 3. Fokusmanagement: wann und wie Fokus programmatisch setzen
- 4. ARIA-Attribute in Vue: aria-label, aria-live und Rollen
- 5. Keyboard-Navigation: Tabs, Pfeiltasten und Escape
- 6. Barrierefreie Modals und Dialoge in Vue
- 7. Barrierefreie Formulare und Fehlermeldungen
- 8. Typische Accessibility-Fehler in Vue-Projekten
- 9. Accessibility-Patterns im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Accessibility in Vue-Komponenten entscheidend ist
Accessibility in Vue-Komponenten ist kein nachträgliches Refactoring, sondern eine Designentscheidung, die von der ersten Zeile Code an die Qualität der Anwendung bestimmt. Der European Accessibility Act verpflichtet ab Juni 2025 alle B2C-Anbieter in der EU, digitale Produkte barrierefrei zu gestalten. Aber jenseits der rechtlichen Anforderungen ist Accessibility auch aus technischen Gründen sinnvoll: semantisches HTML verbessert SEO, Keyboard-Navigation hilft Power-Usern, und Fokusmanagement macht Single-Page-Apps für alle Nutzenden zuverlässiger.
Vue 3's Composition API bietet ideale Voraussetzungen für wiederverwendbare Accessibility-Lösungen. Composables wie useFocusTrap oder useAnnouncer kapseln komplexe Accessibility-Logik in testbare Einheiten, die in jeder Komponente konsistent eingesetzt werden können. Custom Directives wie v-focus oder v-trap-focus binden Accessibility-Verhalten deklarativ an DOM-Elemente – ohne Boilerplate in jeder Komponente. Accessibility in Vue-Komponenten ist also nicht mehr Arbeit, sondern lediglich das Schreiben von Code, der denselben Standards folgt wie der Rest des Projekts.
2. Semantisches HTML als Fundament
Das stärkste Werkzeug für Accessibility in Vue-Komponenten ist bereits im Browser eingebaut: semantisches HTML. Ein <button>-Element ist von Haus aus per Tastatur fokussierbar, aktiviert bei Enter und Space, und wird von Screenreadern korrekt als Schaltfläche angekündigt. Ein <div> mit Click-Handler macht dasselbe visuell, ist aber für Tastaturnutzer und Screenreader unsichtbar. Das Accessibility-Grundprinzip in Vue: Niemals ein nicht-interaktives Element mit einem Click-Handler versehen, ohne auch Rolle, Fokussierbarkeit und Keyboard-Events zu ergänzen. Der Aufwand dafür übersteigt bei weitem den, ein semantisch korrektes Element von Anfang an zu nutzen.
In Vue-Komponenten bedeutet das: Listen mit <ul> und <li>, Navigationsbereiche mit <nav>, Hauptinhalt mit <main> und Landmarks für Screenreader. Überschriften-Hierarchien (h1 bis h6) müssen die Dokumentstruktur widerspiegeln – keine h3 nach h1 ohne h2 dazwischen. In Single-Page-Apps mit Vue Router ist die korrekte Überschriften-Hierarchie besonders wichtig, weil sich der Seiteninhalt ohne Reload ändert und Screenreader die neue Struktur erst aktiv erkunden müssen. Das Accessibility-Pattern für Vue Router: nach jedem Routenwechsel den Fokus auf die Haupt-Überschrift der neuen Seite setzen und den neuen Seitentitel an eine ARIA-Live-Region melden.
<!-- BaseButton.vue — Semantic, accessible button component -->
<script setup lang="ts">
interface Props {
variant?: 'primary' | 'secondary' | 'danger'
loading?: boolean
disabled?: boolean
ariaLabel?: string
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
loading: false,
disabled: false,
})
// Emits with explicit typing
const emit = defineEmits<{ click: [event: MouseEvent] }>()
// Computed disabled state — also disabled while loading
const isDisabled = computed(() => props.disabled || props.loading)
</script>
<template>
<!-- Native <button> — keyboard-focusable and role="button" for free -->
<button
:disabled="isDisabled"
:aria-disabled="isDisabled"
:aria-busy="loading"
:aria-label="ariaLabel"
:class="['btn', `btn-${variant}`, { 'btn-loading': loading }]"
@click="!isDisabled && emit('click', $event)"
>
<!-- Spinner visually shown, hidden from screenreaders -->
<span v-if="loading" aria-hidden="true" class="spinner" />
<!-- Screen-reader-only loading text -->
<span v-if="loading" class="sr-only">Wird geladen…</span>
<slot v-else />
</button>
</template>
3. Fokusmanagement: wann und wie Fokus programmatisch setzen
Fokusmanagement ist das komplexeste Kapitel der Accessibility in Vue-Komponenten. In klassischen Server-gerenderten Seiten setzt der Browser den Fokus nach einem Seitenladung automatisch auf den Anfang des Dokuments – in einer SPA ändert sich der Inhalt, ohne dass die Seite neu lädt, und der Fokus bleibt wo er war. Das führt zu Situationen, in denen ein Tastaturnutzer auf "Bestellen" klickt, der Bestellvorgang abläuft, und der Fokus danach auf einem Button liegt, der gar nicht mehr existiert oder sich weit weg vom neuen Inhalt befindet.
Das Accessibility-Pattern in Vue für Fokusmanagement nutzt useTemplateRef und nextTick in Kombination. Nach einer asynchronen Operation oder einem Routenwechsel wird nextTick abgewartet, bevor element.focus() aufgerufen wird – sichergestellt, dass der DOM bereits aktualisiert ist. Ein useFocusTrap-Composable hält den Fokus innerhalb eines Bereichs – unverzichtbar für Modals, Drawers und Dropdown-Menus. Das Composable merkt sich das zuvor fokussierte Element und stellt es beim Schließen wieder her, damit Tastaturnutzer dort weitermachen können, wo sie aufgehört haben. Accessibility in Vue erfordert diese Art von aktivem Fokusmanagement überall, wo sich der sichtbare Inhalt ohne Nutzerinitiative einer Tab-Navigation verändert.
// composables/useFocusTrap.ts — Trap focus within a container
import { ref, onUnmounted, type Ref } from 'vue'
const FOCUSABLE_SELECTORS = [
'a[href]', 'button:not([disabled])', 'input:not([disabled])',
'select:not([disabled])', 'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
].join(', ')
export function useFocusTrap(containerRef: Ref<HTMLElement | null>) {
const previouslyFocused = ref<HTMLElement | null>(null)
function getFocusable(): HTMLElement[] {
if (!containerRef.value) return []
return Array.from(containerRef.value.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS))
}
function handleKeydown(event: KeyboardEvent) {
if (event.key !== 'Tab') return
const focusable = getFocusable()
if (!focusable.length) return
const first = focusable[0]
const last = focusable[focusable.length - 1]
if (event.shiftKey && document.activeElement === first) {
event.preventDefault()
last.focus()
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault()
first.focus()
}
}
function activate() {
previouslyFocused.value = document.activeElement as HTMLElement
document.addEventListener('keydown', handleKeydown)
// Focus first focusable element inside container
const focusable = getFocusable()
focusable[0]?.focus()
}
function deactivate() {
document.removeEventListener('keydown', handleKeydown)
previouslyFocused.value?.focus()
previouslyFocused.value = null
}
onUnmounted(deactivate)
return { activate, deactivate }
}
4. ARIA-Attribute in Vue: aria-label, aria-live und Rollen
ARIA (Accessible Rich Internet Applications) ergänzt semantisches HTML dort, wo HTML-Elemente allein nicht ausreichen, um die Bedeutung einer Interaktion zu vermitteln. In Vue bindet man ARIA-Attribute genauso wie andere Attribute – reaktiv mit :aria-expanded="isOpen" oder statisch mit aria-label="Suchfeld". Das wichtigste Prinzip für Accessibility in Vue-Komponenten: ARIA-Attribute nie ohne semantisches HTML-Fundament einsetzen. Ein role="button" auf einem <div> deklariert die Rolle, aber nicht die Fokussierbarkeit oder das Keyboard-Verhalten – das muss alles manuell ergänzt werden, was die Verwendung von <button> in jedem Fall einfacher macht.
ARIA-Live-Regionen sind das entscheidende Werkzeug für dynamische Inhaltsänderungen in Vue SPAs. Ein Element mit aria-live="polite" wird von Screenreadern vorgelesen, sobald sich sein Inhalt ändert – ohne dass der Nutzer aktiv fokussieren muss. Das Muster für Vue: Eine unsichtbare <div aria-live="polite" aria-atomic="true">-Region im App-Root, die über ein Composable useAnnouncer mit Statusmeldungen befüllt wird. Wenn die Suchergebnisse geladen sind, wenn ein Formular erfolgreich abgesendet wurde, wenn eine Fehlermeldung erscheint – all das gehört in den Announcer, damit Screenreader-Nutzer denselben Informationsstand haben wie sehende Nutzer.
5. Keyboard-Navigation: Tabs, Pfeiltasten und Escape
Vollständige Keyboard-Navigation ist das Herzstück der Accessibility in Vue-Komponenten. Die Tab-Reihenfolge folgt der DOM-Reihenfolge – das bedeutet, dass CSS-Umsortierungen mit order oder flex-direction: column-reverse die visuelle und die Keyboard-Reihenfolge auseinanderbringen können, was desorientierend für Tastaturnutzer ist. In Vue sollte die DOM-Reihenfolge immer mit der visuellen Lese- und Interaktionsreihenfolge übereinstimmen, selbst wenn das gelegentlich andere CSS-Ansätze erfordert.
Für zusammengesetzte Widgets wie Menüleisten, Tabs, ListBoxen und Bäume definiert die ARIA Authoring Practices Guide ein spezifisches Keyboard-Pattern: Tab bewegt den Fokus in das Widget hinein und heraus, Pfeiltasten navigieren innerhalb des Widgets. Das verhindert, dass Tastaturnutzer durch lange Listen von Menüpunkten tabben müssen. In Vue implementiert man dieses Pattern mit einem Composable useRovingFocus, das tabindex="-1" auf alle Elemente setzt außer dem aktiven und die Pfeiltasten-Navigation verwaltet. Escape schließt immer das nächste übergeordnete überlagernde Element – Modal, Dropdown, Tooltip – und gibt den Fokus an das auslösende Element zurück.
// composables/useRovingFocus.ts — Arrow-key navigation for composite widgets
import { ref, type Ref } from 'vue'
export function useRovingFocus(items: Ref<HTMLElement[]>) {
const activeIndex = ref(0)
function setActive(index: number) {
const clamped = Math.max(0, Math.min(index, items.value.length - 1))
// Remove tabindex from all, set on active only
items.value.forEach((el, i) => {
el.setAttribute('tabindex', i === clamped ? '0' : '-1')
})
items.value[clamped]?.focus()
activeIndex.value = clamped
}
function handleKeydown(event: KeyboardEvent) {
const count = items.value.length
if (!count) return
switch (event.key) {
case 'ArrowDown':
case 'ArrowRight':
event.preventDefault()
setActive((activeIndex.value + 1) % count)
break
case 'ArrowUp':
case 'ArrowLeft':
event.preventDefault()
setActive((activeIndex.value - 1 + count) % count)
break
case 'Home':
event.preventDefault()
setActive(0)
break
case 'End':
event.preventDefault()
setActive(count - 1)
break
}
}
// Initialize: first item focusable, rest not
function init() {
items.value.forEach((el, i) => {
el.setAttribute('tabindex', i === 0 ? '0' : '-1')
})
activeIndex.value = 0
}
return { activeIndex, handleKeydown, setActive, init }
}
6. Barrierefreie Modals und Dialoge in Vue
Modals sind die häufigste Quelle von Accessibility-Fehlern in Vue-Komponenten. Ein nicht barrierefreies Modal lässt Tastaturnutzer durch den Hintergrundinhalt tabben, obwohl das Modal offen ist. Screenreader lesen den Hintergrundinhalt vor, weil er im DOM nicht als "verdeckt" markiert ist. Das Schließen per Escape funktioniert nicht. Das korrekte Accessibility-Pattern für Vue-Modals basiert auf drei Säulen: Fokus-Trap, aria-modal="true" plus role="dialog", und inert-Attribut auf dem Hintergrundinhalt. Das HTML-inert-Attribut macht alle Inhalte eines Elements für Pointer, Keyboard und Screenreader unsichtbar – das eleganteste Werkzeug, um den Hintergrund während eines offenen Modals zu neutralisieren.
Vue's Teleport-Komponente ist der ideale Partner für barrierefreie Modals. Mit <Teleport to="body"> wird das Modal-HTML direkt als Kind des <body> gerendert – unabhängig davon, wo es in der Komponenten-Hierarchie deklariert ist. Das verhindert z-index-Probleme und stellt sicher, dass aria-modal korrekt funktioniert, weil das Modal nicht innerhalb eines Containers mit overflow: hidden sitzt. Der Fokus muss beim Öffnen auf den Schließen-Button oder auf die erste fokussierbare Aktion innerhalb des Dialogs gesetzt werden – nicht auf das umgebende Container-Element, das selbst nicht interaktiv ist.
7. Barrierefreie Formulare und Fehlermeldungen
Formulare sind der Bereich, in dem Accessibility in Vue-Komponenten am direktesten die Nutzbarkeit beeinflusst. Jedes Formularfeld benötigt ein explizit verknüpftes Label – entweder durch <label for="id"> mit passendem id auf dem Input, oder durch aria-labelledby beziehungsweise aria-label. Placeholder-Texte sind kein Ersatz für Labels: Sie verschwinden beim Tippen, haben zu geringen Kontrast und werden von älteren Screenreadern nicht konsistent vorgelesen. In Vue-Formular-Komponenten generiert man IDs dynamisch mit einer Composable useId(), die seit Vue 3.5 als useId() nativ verfügbar ist und garantiert eindeutige IDs auch bei Server-Side-Rendering erzeugt.
Fehlermeldungen müssen programmatisch mit dem Feld verknüpft sein, nicht nur visuell darunter platziert. Das Accessibility-Pattern: aria-describedby auf dem Input zeigt auf die ID der Fehlermeldung, aria-invalid="true" signalisiert den Fehlerzustand. Screenreader lesen beim Fokussieren des Felds automatisch die verknüpfte Fehlermeldung vor. Fehlerzustände, die nach einer async-Validierung erscheinen, gehören zusätzlich in eine ARIA-Live-Region, damit Screenreader-Nutzer unmittelbar informiert werden ohne aktiv das Feld verlassen und wieder betreten zu müssen. Formulare, die diese Accessibility-Standards einhalten, sind nicht nur barrierefreier – sie konvertieren auch besser, weil Nutzende schneller und sicherer ausfüllen können.
8. Typische Accessibility-Fehler in Vue-Projekten
Der häufigste Accessibility-Fehler in Vue-Projekten ist der div-Soup-Antipattern: Interaktive Elemente als <div> mit @click-Handler, ohne Fokussierbarkeit, ohne Rolle und ohne Keyboard-Events. Ein Icon-Button ohne sichtbaren Text benötigt ein aria-label – ohne es sagt der Screenreader nur "Schaltfläche", ohne zu erklären, was sie tut. v-if und v-show verhalten sich für Screenreader unterschiedlich: v-if entfernt das Element komplett aus dem DOM, v-show setzt nur display: none. Mit v-show ausgeblendete Elemente sind für Screenreader nicht mehr zugänglich, da display: none das Element aus dem Accessibility-Tree entfernt. Wer Inhalte vor sehenden Nutzern verbergen, aber für Screenreader sichtbar lassen möchte, nutzt die sr-only-CSS-Klasse.
Ein zweiter verbreiteter Fehler: Farbkontrast wird nach dem Entwickeln als Problem entdeckt, nicht während. Das WCAG 2.2 AA-Kriterium fordert mindestens 4,5:1 Kontrastverhältnis für normalen Text und 3:1 für großen Text (ab 18pt oder 14pt fett). Tailwind CSS-Klassen wie text-gray-400 auf weißem Hintergrund fallen regelmäßig durch dieses Kriterium. In Vue-Projekten lohnt sich die Integration von axe-core oder @axe-core/vue als Dev-Dependency, die Accessibility-Verstöße direkt in der Browser-Konsole während der Entwicklung meldet – damit Probleme gefunden werden, bevor sie in Production kommen.
9. Accessibility-Patterns im direkten Vergleich
In Vue-Projekten gibt es für viele Interaktionsmuster eine zugängliche und eine nicht zugängliche Variante. Die Wahl hat direkte Konsequenzen für die WCAG-Konformität und die Nutzbarkeit für Menschen mit Behinderungen.
| Szenario | Nicht zugänglich | Barrierefreies Pattern | WCAG-Kriterium |
|---|---|---|---|
| Schaltfläche | <div @click> |
<button> |
4.1.2 Name, Rolle, Wert |
| Icon-Button | Kein Label | aria-label="Schließen" |
1.1.1 Textalternative |
| Fehlermeldung | Nur visuell darunter | aria-describedby + aria-invalid |
3.3.1 Fehlererkennung |
| Modal | Kein Fokus-Trap | useFocusTrap + inert |
2.1.2 Keine Tastaturfalle |
| Routenwechsel | Fokus bleibt wo er war | Fokus auf h1, Announce Seite | 2.4.3 Fokus-Reihenfolge |
Die Tabelle zeigt, dass jedes Accessibility-Problem einem konkreten WCAG-Kriterium zugeordnet ist – und dass die barrierefreie Variante in Vue-Projekten fast immer auf nativen HTML-Elementen und ARIA-Attributen basiert, nicht auf aufwändigen Custom-Implementierungen. Das Accessibility-Pattern ist oft die einfachere und robustere Lösung, nicht die kompliziertere.
Mironsoft
Vue 3 Accessibility-Audits, WCAG-Umsetzung und barrierefreie Komponentenbibliotheken
Vue-App, die WCAG 2.2 AA erfüllt?
Wir prüfen bestehende Vue-Applikationen auf Accessibility-Verstöße und implementieren barrierefreie Patterns – Fokusmanagement, ARIA, Keyboard-Support und Farbkontraste für den European Accessibility Act.
A11y-Audit
Automatisierte Prüfung mit axe-core und manuelle Screenreader-Tests mit NVDA und VoiceOver
Composables
useFocusTrap, useAnnouncer, useRovingFocus – wiederverwendbare Accessibility-Bausteine
WCAG-Umsetzung
AA-Konformität für alle kritischen User-Flows – Formulare, Modals, Navigation und Benachrichtigungen
10. Zusammenfassung
Die Umsetzung von Accessibility in Vue-Komponenten ist keine einmalige Aufgabe, sondern eine kontinuierliche Praxis. Semantisches HTML liefert das stärkste Fundament – native Elemente bringen Fokussierbarkeit, Rollen und Keyboard-Verhalten kostenlos mit. Fokusmanagement mit useFocusTrap und gezieltem focus() nach nextTick stellt sicher, dass Tastaturnutzer in SPAs immer wissen, wo sie sind. ARIA-Attribute wie aria-live, aria-expanded und aria-describedby vermitteln dynamische Zustandsänderungen an Screenreader. Keyboard-Navigation mit Roving-Tabindex in zusammengesetzten Widgets entspricht den ARIA Authoring Practices.
Der größte Hebel in Vue-Projekten liegt in der Standardisierung über Composables und Basis-Komponenten. Wer BaseButton, BaseModal und BaseFormField mit vollständiger Accessibility-Implementierung einmal richtig baut, bekommt diese Qualität in allen Komponenten, die diese Bausteine nutzen – ohne dass jedes Team-Mitglied alle ARIA-Patterns im Kopf haben muss. Accessibility in Vue-Komponenten skaliert über gute Architektur, nicht über individuelle Expertise bei jedem Commit.
Accessibility in Vue-Komponenten — Das Wichtigste auf einen Blick
Semantisches HTML
Immer native Elemente nutzen: <button>, <nav>, <main>. Kein <div @click> ohne vollständige ARIA-Ergänzung.
Fokusmanagement
useFocusTrap für Modals, nextTick + focus() nach Routenwechsel, vorheriges Element beim Schließen wiederherstellen.
ARIA-Live
useAnnouncer für dynamische Statusmeldungen. aria-describedby für Fehlermeldungen. aria-expanded für toggle-fähige Bereiche.
Tools
@axe-core/vue in Dev-Modus, NVDA/VoiceOver manuell testen, Lighthouse Accessibility-Score in CI integrieren.