WCAG 2.2 Level AA in React und Next.js: Der technische Accessibility-Audit vor der EAA-Durchsetzung

#WCAG 2.2 Level AA React

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

LinkedIn

Die Compliance-Frist ist keine Gerüchte. Der European Accessibility Act verpflichtet digitale Produkte und Dienstleistungen, die in der EU verkauft werden, zur Erfüllung von WCAG 2.2 Level AA - und die Durchsetzung für 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-Konformitätserklärungen. Verkaufszyklen stocken.

Das Problem ist, dass die meisten Entwicklerteams eine WCAG-2.2-Checkliste erhalten, die für Personen geschrieben wurde, die Websites mit Browser-Erweiterungen auditieren, nicht für 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 Lücke, die dieser Beitrag mit einem technischen Accessibility-Audit adressiert.

Warum React und Next.js Barrierefreiheitsprobleme erzeugen, die statische Sites nicht haben

WCAG wurde ursprünglich gegen das mentale Modell eines einmalig ladenden Dokuments geschrieben. React-Anwendungen sind keine Dokumente - sie sind Zustandsmaschinen, die den DOM gezielt aktualisieren. Diese architektonische Inkompatibilität erzeugt eine Kategorie von Barrierefreiheitsfehlern, die Browser-Erweiterungs-Scanner nicht erkennen und die die meisten WCAG-Checklisten nicht präzise genug beschreiben, um sie zu beheben.

Routenwechsel verwerfen den Fokus. In einer traditionellen mehrseitigen Anwendung löst das Navigieren zu einer neuen URL einen vollständigen Seitenlade-Vorgang aus, und Screenreader kündigen den neuen Seitentitel automatisch an. In einer Next.js-App-Router-Anwendung erfolgt die Navigation clientseitig. Der DOM aktualisiert sich, die URL ändert 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 Ankündigung, dass sich etwas geändert 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 standardmäßig das erste fokussierbare Element, was oft eine Schließen-Schaltfläche ist. Wenn ein Nutzer einen Bestätigungs-Dialog öffnet, ist das erste, was ein Screenreader ankündigt, "Schließen-Schaltfläche" - bevor der Nutzer weiß, was er schließt oder bestätigt. Dieses Verhalten besteht automatisierte Scans und scheitert an realer Usability.

Asynchrone Zustandsaktualisierungen erzeugen Timing-Lücken. Formularvalidierungsfehler 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 anzukündigen. Wenn das Fehler-<span> einfach in den DOM eingebunden wird, hört ein Nutzer, der sich auf einen Screenreader verlässt, es möglicherweise nie. Live-Regionen, aria-describedby und Fokus-Management müssen bewusst koordiniert werden - und die Koordination muss Neurenderings überstehen.

Die WCAG-2.2-Kriterien, die React-Teams treffen

WCAG 2.2 fügten sechs neue Erfolgskriterien über WCAG 2.1 hinaus hinzu. Zwei von ihnen treffen React-Codebasen konsequent.

2.5.7 Ziehbewegungen (Level AA)

Jede Funktionalität, die eine Ziehbewegung verwendet, muss auch ohne Ziehen erreichbar sein. Das klingt unkompliziert, bis du echte Produktoberflächen betrachtest: Größen-veränderbare 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 Durchführung eines WCAG-2.5.7-Audits auf einem SaaS-Dashboard gibt typischerweise in einem Nachmittag sechs bis zwölf Verletzungen preis.

Der Fix ist nicht, die Drag-Funktionalität zu entfernen. Es ist, eine Zeiger-Event-Alternative hinzuzufügen - typischerweise Tastatursteuerung für Neuordnungsoperationen (Pfeiltasten mit expliziten Move-Handlern) und barrierefreie Größen-Änderungs-Handles mit role="separator" und aria-valuenow-Attributen für Größen-veränderbare Bereiche.

2.4.11 Fokus nicht verdeckt und 2.4.12 Fokus nicht verdeckt (Erweitert)

Das Mindestkriterium erfordert, dass eine fokussierte Komponente nicht vollständig durch Sticky-Header, Cookie-Banner oder Benachrichtigungs-Toasts verdeckt ist. Das erweiterte Kriterium erfordert, dass das fokussierte Element vollständig 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-Höhe, aber das erfordert die Kenntnis der Header-Höhe zur Scroll-Zeit - die in einem responsiven Layout bei Breakpoints wechselt und sich dynamisch ändern kann, wenn der Header kollabiert.

Eine Produktionslösung 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-Höhe dynamisch ist - zum Beispiel beim Scrollen kollabiert - aktualisiere die Custom-Property mit einem ResizeObserver, der am Header-Element hängt.

Fokus-Management im Next.js App Router

Der App Router führte Layouts, verschachtelte Routen und Streaming-Rendering ein - alles davon erzeugt neue Herausforderungen für das Fokus-Management. Das empfohlene Muster für 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, während gleichzeitig die neue Seite über eine ARIA-Live-Region angekündigt 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 Navigationsankündigung und einen vorhersehbaren Fokus-Landepunkt. Der requestAnimationFrame-Wrapper für die Live-Region-Aktualisierung erzwingt eine erneute Ankündigung, auch wenn der Seitentitel unverändert bleibt - ohne ihn unterdrücken manche Screenreader eine wiederholte identische Ankündigung.

Formularfehler-Ankündigungen, 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-Einfügungen nicht ankündigen, es sei denn, sie erfolgen innerhalb einer designierten Live-Region.

Das Produktionsmuster kombiniert drei Mechanismen: eine ARIA-Live-Region für Ankündigungen, aria-describedby, das das Eingabefeld mit seiner Fehlermeldung verknüpft, 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 für Validierungsfehler - assertive Regionen unterbrechen den Screenreader mitten im Satz, was angemessen ist, wenn der Nutzer nicht fortfahren kann, aber störend, wenn es für informative Meldungen übermäßig genutzt wird.

Bei einem Formulareinreichungsfehler verschiebt sich der Fokus auf das erste ungültige 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 über Theme-Wechsel

WCAG 1.4.3 erfordert ein Mindestkontrastverhältnis von 4,5:1 für normalen Text und 3:1 für großen Text und UI-Komponentengrenzen. Dieses über helle und dunkle Themes hinweg einzuhalten ist der Bereich, in dem die meisten Teams stille Fehler ansammeln - nicht weil jemand eine unüberdachte Entscheidung getroffen hat, sondern weil semantische Tokens auf eine Weise zusammengesetzt werden, die nicht offensichtlich ist, bis man sie misst.

Eine gedämpfte Textfarbe, die im hellen Modus 4,8:1 gegen einen weißen Hintergrund erreicht, kann im dunklen Modus gegen eine fast schwarze Fläche auf 2,9:1 fallen. Der Fehler liegt darin, Tokens einzeln zu verifizieren, anstatt die Verhältnisse 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);
  }
}

Führe 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 tatsächlich gerenderten Seiten deiner Anwendung ausführen, einschließlich authentifizierter Zustände und geöffneter 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 fängt ungefähr 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 ausgelöst 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. Ankündigungsqualität, Fokus-Reihenfolge und die Kohärenz dessen, was ein Screenreader vermittelt, können nur durch Zuhören bewertet werden.

Nur-Tastatur-Navigation testen. Tabulator-navigiere durch jedes interaktive Element in der Reihenfolge. Bestätige, dass der Fokus immer sichtbar ist, nie unbeabsichtigt gefangen ist und immer an eine logische Position zurückkehrt, wenn ein Modal, ein Drawer oder ein Popover schließt.

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 Oberflächen - Dashboards, Datentabellen, mehrstufige Formulare - und vergewissere dich, dass alle Funktionen erreichbar bleiben.

Loslegen ohne das Roadmap zu stoppen

Eine vollständige 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. Formularfehler-Ankündigungen blockieren den Aufgabenabschluss und sind als nächstes am wirkungsvollsten. Probleme auf Komponentenebene im Design-System kommen danach, beginnend mit interaktiven Komponenten auf deinen meist frequentierten Seiten.

Dokumentiere jede Korrektur während der Arbeit. Procurement-Teams fragen zunehmend nach einem VPAT oder einer Konformitätserklärung, und deine Pull-Request-Historie - richtig strukturiert - wird zum Prüfpfad, der sie unterstützt.

Barrierefreiheits-Sanierung in komplexen React- und Next.js-Anwendungen ist Teil der Anwendungsentwicklungs- und Code-Qualitätsarbeit, die wir mit B2B-SaaS-Teams machen. Wenn die EAA-Durchsetzung jetzt auf deiner Roadmap ist und du eine unabhängige technische Bewertung deiner WCAG-2.2-Lücken benötigst - und eine realistische Schätzung, was die Sanierung erfordern wird - melde dich unter hello@wolf-tech.io oder besuche wolf-tech.io.


Häufig gestellte Fragen

Ersetzt WCAG 2.2 WCAG 2.1 vollständig?

WCAG 2.2 ist rückwärtskompatibel - alle WCAG-2.1-Erfolgskriterien sind in 2.2 enthalten. WCAG 2.2 Level AA zu erfüllen bedeutet, auch WCAG 2.1 Level AA zu erfüllen. Für 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 Konformitätsbewertungen einbezogen werden.

Fängt axe-core alle WCAG-2.2-Kriterien automatisch ab?

Nein. Axe fängt zuverlässig strukturelle Verletzungen ab: fehlende Labels, ungültiges ARIA-Gebrauch, 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 Formularfehler in einer nutzbaren Reihenfolge ankündigt. 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-Öffnung, Tastaturnavigation in zusammengesetzten Widgets und Ankündigungsqualität hängen alle davon ab, wie dein Team die Primitive zusammensetzt und konfiguriert. Ein Radix-basiertes Design-System kann vollständig WCAG-2.2-AA-konform sein, erfordert aber bewusste Arbeit auf dem, was Radix standardmäßig bietet.

Wie lange dauert eine WCAG-2.2-AA-Sanierung typischerweise für ein ausgereiftes Next.js-SaaS?

Für ein Produkt mit 50-150 einzigartigen UI-Komponenten und keinerlei vorheriger Barrierefreiheitsinvestition ist eine realistische Schätzung 6-10 Wochen Entwicklerzeit für einen Entwickler, der hauptsächlich an der Sanierung arbeitet, vorausgesetzt, das Design-Token-System erfordert keine vollständige Überarbeitung. Routenwechsel- und Formularfehler-Fixes nehmen typischerweise einige Tage in Anspruch. Probleme auf Komponentenebene - benutzerdefiniertes Modal-Fokus-Management, Tastaturunterstützung für Zieh-Interaktionen, komplexe Datentabellen-Navigation - machen den Großteil der verbleibenden Zeit aus.

Sagt uns unser bestehender Lighthouse-Barrierefreiheits-Score, ob wir WCAG 2.2 AA bestehen?

Nicht zuverlässig. Lighthouse führt axe unter der Haube aus und zeigt eine Teilmenge automatisierter Prüfungen. Ein Lighthouse-Barrierefreiheits-Score von 100 ist konsistent mit Dutzenden realer WCAG-Fehler, die automatisierte Tools nicht erkennen können. Verwende Lighthouse als Bodenprüfung, nicht als Compliance-Signal.