WCAG 2.2 Level AA in React und Next.js: Der technische Accessibility-Audit vor der EAA-Durchsetzung
Die Compliance-Frist ist keine Geruechte. Der European Accessibility Act verpflichtet digitale Produkte und Dienstleistungen, die in der EU verkauft werden, zur Erfullung von WCAG 2.2 Level AA - und die Durchsetzung fuer B2B-SaaS wechselte von "angestreb" zu "durchsetzbar" Mitte 2025, wobei die Mitgliedstaaten Strafmechanismen schrittweise bis 2026 aktivieren. Wenn dein Produkt von Enterprise-Kunden in Deutschland, Frankreich, den Niederlanden oder Spanien genutzt wird, stellen Procurement-Teams bereits Anfragen nach Barrierefreiheits-Konformitaetserklaerungen. Verkaufszyklen stocken.
Das Problem ist, dass die meisten Entwicklerteams eine WCAG-2.2-Checkliste erhalten, die fuer Personen geschrieben wurde, die Websites mit Browser-Erweiterungen auditieren, nicht fuer Entwickler, die eine 200-Komponenten-React-Codebasis mit einem gemeinsam genutzten Design-System auf Basis von Radix UI und Headless UI pflegen. Die Checkliste sagt "sichtbare Fokus-Indikatoren bereitstellen". Sie sagt nicht, dass deine Next.js-App-Router-Navigation die Fokusposition bei jedem Routenwechsel still vernichtet. Das ist die Luecke, die dieser Beitrag adressiert.
Warum React und Next.js Barrierefreiheitsprobleme erzeugen, die statische Sites nicht haben
WCAG wurde urspruenglich gegen das mentale Modell eines einmalig ladenden Dokuments geschrieben. React-Anwendungen sind keine Dokumente - sie sind Zustandsmaschinen, die den DOM gezielt aktualisieren. Diese architektonische Inkompatibilitaet erzeugt eine Kategorie von Barrierefreiheitsfehlern, die Browser-Erweiterungs-Scanner nicht erkennen und die die meisten WCAG-Checklisten nicht praezise genug beschreiben, um sie zu beheben.
Routenwechsel verwerfen den Fokus. In einer traditionellen mehrseitigen Anwendung loest das Navigieren zu einer neuen URL einen vollstaendigen Seitenlade-Vorgang aus, und Screenreader kuendigen den neuen Seitentitel automatisch an. In einer Next.js-App-Router-Anwendung erfolgt die Navigation clientseitig. Der DOM aktualisiert sich, die URL aendert sich, aber ohne explizites Fokus-Management bleibt der Screenreader-Cursor dort, wo er war. Ein Tastaturnutzer, der einen Link in der Hauptnavigation geklickt hat, ist jetzt irgendwo im DOM der alten Seite verloren, ohne Ankuendigung, dass sich etwas geaendert hat.
Komponentenbibliothek-Standardwerte sind keine barrierefreien Standardwerte. Radix UI und Headless UI bieten ausgezeichnete Barrierefreiheits-Primitive, aber "Primitiv" ist das entscheidende Wort - sie stellen die Bausteine bereit, keine fertigen barrierefreien Komponenten. Die Radix-Dialog-Komponente trapped den Fokus korrekt innerhalb eines offenen Modals, aber ihr onOpenAutoFocus-Prop fokussiert standardmaessig das erste fokussierbare Element, was oft eine Schliessen-Schaltflaeche ist. Wenn ein Nutzer einen Bestaetigungs-Dialog oeffnet, ist das erste, was ein Screenreader ankuendigt, "Schliessen-Schaltflaeche" - bevor der Nutzer weiss, was er schliesst oder bestaetigt. Dieses Verhalten besteht automatisierte Scans und scheitert an realer Usability.
Asynchrone Zustandsaktualisierungen erzeugen Timing-Luecken. Formulaervalidierungsfehler in React werden oft bedingt basierend auf dem Komponentenzustand gerendert. Das Problem ist, dass wenn React den DOM aktualisiert, um eine Fehlermeldung anzuzeigen, der Screenreader keinen automatischen Mechanismus hat, sie anzukuendigen. Wenn das Fehler-<span> einfach in den DOM eingebunden wird, hoert ein Nutzer, der sich auf einen Screenreader verlasst, es moeglicherweise nie. Live-Regionen, aria-describedby und Fokus-Management muessen bewusst koordiniert werden - und die Koordination muss Neurenderings ueberstehen.
Die WCAG-2.2-Kriterien, die React-Teams treffen
WCAG 2.2 fuegten sechs neue Erfolgskriterien ueber WCAG 2.1 hinaus hinzu. Zwei von ihnen treffen React-Codebasen konsequent.
2.5.7 Ziehbewegungen (Level AA)
Jede Funktionalitaet, die eine Ziehbewegung verwendet, muss auch ohne Ziehen erreichbar sein. Das klingt unkompliziert, bis du echte Produktoberflaechen betrachtest: Groessen-veraenderbare Seitenleisten, Drag-to-Reorder-Listenelemente, interaktive Timeline-Scrubber, Dashboard-Widget-Anordnung. Die meisten Komponentenbibliotheken implementieren diese als Pointer-Event-Listener, die keine Tastatur- oder Einzelzeiger-Alternative haben. Die Durchfuehrung eines WCAG-2.5.7-Audits auf einem SaaS-Dashboard gibt typischerweise in einem Nachmittag sechs bis zwoelf Verletzungen preis.
Der Fix ist nicht, die Drag-Funktionalitaet zu entfernen. Es ist, eine Zeiger-Event-Alternative hinzuzufuegen - typischerweise Tastatursteuerung fuer Neuordnungsoperationen (Pfeiltasten mit expliziten Move-Handlern) und barrierefreie Groessen-Aenderungs-Handles mit role="separator" und aria-valuenow-Attributen fuer Groessen-veraenderbare Bereiche.
2.4.11 Fokus nicht verdeckt und 2.4.12 Fokus nicht verdeckt (Erweitert)
Das Mindestkritrerium erfordert, dass eine fokussierte Komponente nicht vollstaendig durch Sticky-Header, Cookie-Banner oder Benachrichtigungs-Toasts verdeckt ist. Das erweiterte Kriterium erfordert, dass das fokussierte Element vollstaendig sichtbar ist. Sticky-Navigationsleisten in Next.js-Layouts verdecken das fokussierte Element beim Scrollen in der Mehrheit realer Codebasen. Der kanonische Fix ist scroll-margin-top auf fokussierbaren Elementen, dimensioniert passend zur Sticky-Header-Hoehe, aber das erfordert die Kenntnis der Header-Hoehe zur Scroll-Zeit - die in einem responsiven Layout bei Breakpoints wechselt und sich dynamisch aendern kann, wenn der Header kollabiert.
Eine Produktionsloesung nutzt eine CSS-Custom-Property, die die Layout-Komponente setzt und aktualisiert, und fokussierbare Komponenten konsumieren:
:root {
--sticky-header-height: 64px;
}
@media (max-width: 768px) {
:root {
--sticky-header-height: 48px;
}
}
*:focus {
scroll-margin-top: calc(var(--sticky-header-height) + 8px);
}
Wenn die Header-Hoehe dynamisch ist - zum Beispiel beim Scrollen kollabiert - aktualisiere die Custom-Property mit einem ResizeObserver, der am Header-Element haengt.
Fokus-Management im Next.js App Router
Der App Router fuehrte Layouts, verschachtelte Routen und Streaming-Rendering ein - alles davon erzeugt neue Herausforderungen fuer das Fokus-Management. Das empfohlene Muster fuer Routenwechsel in einer App-Router-Anwendung ist, ein visuell verborgenes Fokusziel am Anfang jeder Seite zu rendern und nach abgeschlossener Navigation programmatisch den Fokus darauf zu verschieben, waehrend gleichzeitig die neue Seite ueber eine ARIA-Live-Region angekuendigt wird.
'use client';
import { usePathname } from 'next/navigation';
import { useEffect, useRef } from 'react';
export function RouteAnnouncer() {
const pathname = usePathname();
const announcerRef = useRef<HTMLParagraphElement>(null);
const focusTargetRef = useRef<HTMLHeadingElement>(null);
useEffect(() => {
const title = document.title || pathname;
if (announcerRef.current) {
announcerRef.current.textContent = '';
requestAnimationFrame(() => {
if (announcerRef.current) {
announcerRef.current.textContent = `Zu ${title} navigiert`;
}
});
}
focusTargetRef.current?.focus({ preventScroll: true });
}, [pathname]);
return (
<>
<p
ref={announcerRef}
aria-live="polite"
aria-atomic="true"
className="sr-only"
/>
<h1
ref={focusTargetRef}
tabIndex={-1}
className="sr-only"
>
{/* Wird vom Seiten-Layout befuellt */}
</h1>
</>
);
}
Platziere diese Komponente in deinem Root-Layout. Screenreader-Nutzer erhalten bei jedem Routenwechsel eine konsistente Navigationsankuendigung und einen vorhersehbaren Fokus-Landepunkt. Der requestAnimationFrame-Wrapper fuer die Live-Region-Aktualisierung erzwingt eine erneute Ankuendigung, auch wenn der Seitentitel unveraendert bleibt - ohne ihn unterdruecken manche Screenreader eine wiederholte identische Ankuendigung.
Formulaerfehler-Ankuendigungen, die mit echten Screenreadern funktionieren
Die naive Implementierung von Formularvalidierungsfehlern baut ein Fehler-<div> neben dem fehlerhaften Feld ein. Das funktioniert visuell und besteht automatisierte Barrierefreiheits-Scans. Es scheitert bei NVDA, JAWS und VoiceOver, weil diese Screenreader DOM-Einfuegungen nicht ankuendigen, es sei denn, sie erfolgen innerhalb einer designierten Live-Region.
Das Produktionsmuster kombiniert drei Mechanismen: eine ARIA-Live-Region fuer Ankuendigungen, aria-describedby, das das Eingabefeld mit seiner Fehlermeldung verknuepft, und programmatisch verschobenen Fokus auf den ersten Fehler bei einem Einreichungsfehler.
import { useId } from 'react';
interface FieldProps {
label: string;
error?: string;
value: string;
onChange: (v: string) => void;
}
export function AccessibleField({ label, error, value, onChange }: FieldProps) {
const inputId = useId();
const errorId = useId();
return (
<div>
<label htmlFor={inputId}>{label}</label>
<input
id={inputId}
aria-describedby={error ? errorId : undefined}
aria-invalid={!!error}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
{error && (
<span id={errorId} role="alert">
{error}
</span>
)}
</div>
);
}
role="alert" erstellt eine implizite Live-Region mit aria-live="assertive". Verwende es nur fuer Validierungsfehler - assertive Regionen unterbrechen den Screenreader mitten im Satz, was angemessen ist, wenn der Nutzer nicht fortfahren kann, aber stoerend, wenn es fuer informative Meldungen uebermaessig genutzt wird.
Bei einem Formulareinreichungsfehler verschiebt sich der Fokus auf das erste ungueltige Feld, nachdem React seinen DOM-Update abgeschlossen hat:
const firstErrorRef = useRef<HTMLInputElement>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const errors = validate(formData);
if (Object.keys(errors).length > 0) {
setErrors(errors);
requestAnimationFrame(() => {
firstErrorRef.current?.focus();
});
return;
}
// Einreichung fortsetzen
};
Das requestAnimationFrame ist erforderlich. React batchet und flusht Zustandsaktualisierungen asynchron, so dass der synchrone Aufruf von focus() nach setErrors den Fokus verschiebt, bevor das Fehler-Element im DOM existiert, und aria-describedby auf nichts zeigt.
Farbkontrast-Tokens ueber Theme-Wechsel
WCAG 1.4.3 erfordert ein Mindestkontrastverhaltnis von 4,5:1 fuer normalen Text und 3:1 fuer grossen Text und UI-Komponentengrenzen. Dieses ueber helle und dunkle Themes hinweg einzuhalten ist der Bereich, in dem die meisten Teams stille Fehler ansammeln - nicht weil jemand eine unueberdachte Entscheidung getroffen hat, sondern weil semantische Tokens auf eine Weise zusammengesetzt werden, die nicht offensichtlich ist, bis man sie misst.
Eine gedaempfte Textfarbe, die im hellen Modus 4,8:1 gegen einen weissen Hintergrund erreicht, kann im dunklen Modus gegen eine fast schwarze Flaeche auf 2,9:1 fallen. Der Fehler liegt darin, Tokens einzeln zu verifizieren, anstatt die Verhaeltnisse zu verifizieren, die aus der Zusammensetzung entstehen.
Baue Kontrast-Validierung in deine Design-Token-Pipeline ein:
import { wcagContrast } from 'culori';
const pairs = [
{ fg: tokens.light['color-text-primary'], bg: tokens.light['color-surface'] },
{ fg: tokens.dark['color-text-primary'], bg: tokens.dark['color-surface'] },
{ fg: tokens.light['color-text-muted'], bg: tokens.light['color-surface'] },
{ fg: tokens.dark['color-text-muted'], bg: tokens.dark['color-surface'] },
];
for (const { fg, bg } of pairs) {
const ratio = wcagContrast(fg, bg);
if (ratio < 4.5) {
console.error(`KONTRAST-FEHLER: ${fg} auf ${bg} = ${ratio.toFixed(2)}:1`);
process.exit(1);
}
}
Fuehre das als Teil deines Token-Build-Schritts in CI aus. Kontrastfehler werden zu Build-Fehlern, bevor sie eine Staging-Umgebung erreichen. Das ist erheblich schneller, als sie in einem manuellen Audit unter Procurement-Druck zu entdecken.
Das CI-Setup: axe-core und Playwright
Manuelle Tests und Browser-Erweiterungs-Scans finden einen Bruchteil echter Barrierefreiheitsprobleme. Die Kombination aus axe-core und Playwright findet erheblich mehr - automatisch, bei jedem Pull Request.
npm install --save-dev @axe-core/playwright axe-playwright
Schreibe Tests, die axe gegen die tatsaechlich gerenderten Seiten deiner Anwendung ausfuehren, einschliesslich authentifizierter Zustaende und geoeffneter interaktiver Komponenten:
import { test } from '@playwright/test';
import { checkA11y } from 'axe-playwright';
test('Dashboard besteht WCAG-2.2-AA-Audit', async ({ page }) => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
await checkA11y(page, undefined, {
axeOptions: {
runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'] },
},
detailedReport: true,
detailedReportOptions: { html: true },
});
});
test('Bestaetigungs-Dialog besteht axe-Test wenn offen', async ({ page }) => {
await page.goto('/settings');
await page.click('[data-testid="delete-account-button"]');
await page.waitForSelector('[role="dialog"]');
await checkA11y(page, '[role="dialog"]', {
axeOptions: {
runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'] },
},
});
});
Axe faengt ungefaehr 30-40% der WCAG-Verletzungen automatisch ab - Farbkontrastfehler, fehlende Alternativtexte, unbeschriftete Formulareingaben, Landmark-Region-Verletzungen und die mechanischeren WCAG-2.2-Kriterien. Der CI-Bericht zeigt genau, welche Komponente den Fehler ausgeloest hat, welches Kriterium verletzt wird und die DOM-Position des Elements. Ein Entwickler kann darauf reagieren, ohne dass ein Barrierefreiheitsspezialist in der Schleife ist.
Manuelles Testen, das CI nicht ersetzen kann
Automatisierte Tools testen Struktur, nicht Erfahrung. Drei manuelle Testschritte decken ab, was axe nicht kann.
Mit einem echten Screenreader testen. NVDA unter Windows mit Firefox, VoiceOver unter macOS mit Safari und TalkBack unter Android mit Chrome decken die Screenreader- und Browser-Kombinationen ab, die deine Nutzer wahrscheinlich verwenden. Navigiere durch die kritischen Pfade deiner Anwendung ohne Maus. Ankuendigungsqualitaet, Fokus-Reihenfolge und die Kohaerenz dessen, was ein Screenreader vermittelt, koennen nur durch Zuhoeren bewertet werden.
Nur-Tastatur-Navigation testen. Tabulator-naviagiere durch jedes interaktive Element in der Reihenfolge. Bestatige, dass der Fokus immer sichtbar ist, nie unbeabsichtigt gefangen ist und immer an eine logische Position zurueckkehrt, wenn ein Modal, ein Drawer oder ein Popover schliesst.
Bei 400% Zoom mit Reflow-Aktivierung testen. WCAG 1.4.10 erfordert, dass Inhalte bei 400% Zoom auf einem 1280px-Viewport ohne horizontales Scrollen benutzbar bleiben. Sticky-Header kollabieren, Navigation wechselt zu einem mobilen Layout, und die meisten Modals brechen bei diesem Zoom-Level. Teste deine komplexesten Oberflaechen - Dashboards, Datentabellen, mehrstufige Formulare - und vergewissere dich, dass alle Funktionen erreichbar bleiben.
Loslegen ohne das Roadmap zu stoppen
Eine vollstaendige WCAG-2.2-AA-Sanierung einer ausgereiften React-Codebasis wird in Wochen, nicht Tagen, gemessen. Priorisiere nach Nutzungsauswirkung und Verkaufszyklusrisiko. Routenwechsel-Fokus-Management betrifft jeden Tastatur- und Screenreader-Nutzer bei jeder Navigation - behebe das zuerst. Formulaerfehler-Ankuendigungen blockieren den Aufgabenabschluss und sind als naechstes am wirkungsvollsten. Probleme auf Komponentenebene im Design-System kommen danach, beginnend mit interaktiven Komponenten auf deinen meist frequentierten Seiten.
Dokumentiere jede Korrektur waehrend der Arbeit. Procurement-Teams fragen zunehmend nach einem VPAT oder einer Konformitaetserklaerung, und deine Pull-Request-Historie - richtig strukturiert - wird zum Pruefpfad, der sie unterstuetzt.
Barrierefreiheits-Sanierung in komplexen React- und Next.js-Anwendungen ist Teil der Anwendungsentwicklungs- und Code-Qualitaetsarbeit, die wir mit B2B-SaaS-Teams machen. Wenn die EAA-Durchsetzung jetzt auf deiner Roadmap ist und du eine unabhaengige technische Bewertung deiner WCAG-2.2-Luecken benoetigst - und eine realistische Schaetzung, was die Sanierung erfordern wird - melde dich unter hello@wolf-tech.io oder besuche wolf-tech.io.
Haeufig gestellte Fragen
Ersetzt WCAG 2.2 WCAG 2.1 vollstaendig?
WCAG 2.2 ist rueckwaertskompatibel - alle WCAG-2.1-Erfolgskriterien sind in 2.2 enthalten. WCAG 2.2 Level AA zu erfullen bedeutet, auch WCAG 2.1 Level AA zu erfullen. Fuer EAA-Zwecke ist der relevante harmonisierte Standard EN 301 549, der auf WCAG 2.1 verweist, in der Praxis aber nun so interpretiert wird, dass WCAG-2.2-Kriterien in Konformitaetsbewertungen einbezogen werden.
Faengt axe-core alle WCAG-2.2-Kriterien automatisch ab?
Nein. Axe faengt zuverlaessig strukturelle Verletzungen ab: fehlende Labels, ungueltiges ARIA-Gebruik, Kontrast unter dem Schwellenwert, fehlende Landmarks. Es kann nicht testen, ob ein Routenwechsel den Fokus korrekt verwaltet, ob eine Zieh-Interaktion eine Tastatur-Alternative hat, oder ob ein Screenreader Formulaerfehler in einer nutzbaren Reihenfolge ankuendigt. Das erfordert manuelle oder Playwright-orchestrierte Tests.
Unser Design-System verwendet Radix UI. Sind wir mit der Barrierefreiheit abgedeckt?
Radix implementiert ARIA-Muster korrekt auf der Primitiv-Ebene, aber "korrekt implementiertes Primitiv" ist nicht dasselbe wie "barrierefreie Komponente im Kontext". Fokus-Handling bei Dialog-Oeffnung, Tastaturnavigation in zusammengesetzten Widgets und Ankuendigungsqualitaet haengen alle davon ab, wie dein Team die Primitive zusammensetzt und konfiguriert. Ein Radix-basiertes Design-System kann vollstaendig WCAG-2.2-AA-konform sein, erfordert aber bewusste Arbeit auf dem, was Radix standardmaessig bietet.
Wie lange dauert eine WCAG-2.2-AA-Sanierung typischerweise fuer ein ausgereiftes Next.js-SaaS?
Fuer ein Produkt mit 50-150 einzigartigen UI-Komponenten und keinerlei vorheriger Barrierefreiheitsinvestition ist eine realistische Schaetzung 6-10 Wochen Entwicklerzeit fuer einen Entwickler, der hauptsaechlich an der Sanierung arbeitet, vorausgesetzt, das Design-Token-System erfordert keine vollstaendige Ueberarbeitung. Routenwechsel- und Formulaerfehler-Fixes nehmen typischerweise einige Tage in Anspruch. Probleme auf Komponentenebene - benutzerdefiniertes Modal-Fokus-Management, Tastaturunterstuetzung fuer Zieh-Interaktionen, komplexe Datentabellen-Navigation - machen den Grossteil der verbleibenden Zeit aus.
Sagt uns unser bestehender Lighthouse-Barrierefreiheits-Score, ob wir WCAG 2.2 AA bestehen?
Nicht zuverlaessig. Lighthouse fuehrt axe unter der Haube aus und zeigt eine Teilmenge automatisierter Pruefungen. Ein Lighthouse-Barrierefreiheits-Score von 100 ist konsistent mit Dutzenden realer WCAG-Fehler, die automatisierte Tools nicht erkennen koennen. Verwende Lighthouse als Bodenpruefung, nicht als Compliance-Signal.

