Komponenten · Barrierefreiheit nach Framework
Barrierefreiheit in React
React erzeugt keine Barrieren, aber es macht drei davon besonders leicht: Der Fokus geht beim Routenwechsel verloren, weil kein echter Seitenaufruf stattfindet; Komponenten verschachteln div-Wrapper, bis die Semantik verschwindet; und generierte id-Werte kollidieren, sobald dieselbe Komponente zweimal auf einer Seite steht. Alle drei sind mit Bordmitteln zu lösen – useId, Fragmente und ein Fokus-Ref nach jedem Routenwechsel.
Diese Seite behandelt die React-spezifischen Fälle. Die vier Probleme, die in jedem Framework gleich sind, stehen unter Barrierefreiheit nach Framework.
Das Wichtigste in Kürze
-
htmlForstattfor,classNamestattclass– die Attributnamen ändern sich, die Semantik nicht. -
useId()erzeugt kollisionsfreie IDs fürhtmlFor,aria-labelledbyundaria-describedby. Feste ID-Strings in Komponenten sind ein Fehler. - Fragmente (
<>…</>) vermeiden Wrapper-divs, die Listen und Tabellen kaputt machen. - Nach dem Routenwechsel Titel setzen und Fokus programmatisch platzieren – React Router tut das nicht von selbst.
- Portale (
createPortal) sind der saubere Weg für Dialoge; das native<dialog>mitshowModal()bleibt trotzdem die erste Wahl. -
eslint-plugin-jsx-a11yfängt einen erheblichen Teil der mechanischen Fehler schon im Editor ab. -
dangerouslySetInnerHTMLumgeht jede Prüfung – dort landen erfahrungsgemäß die meisten fremden Barrieren. - Stand August 2026.
IDs: useId statt fester Strings
Der häufigste React-spezifische Befund entsteht aus einer wiederverwendbaren Komponente mit fest verdrahteter id. Steht sie zweimal auf einer Seite, zeigen beide Labels auf dasselbe Feld – und das zweite Feld hat keinen Namen mehr. Warum das kein Formfehler, sondern ein echter Verstoß gegen 4.1.2 ist, steht unter Warum WCAG 4.1.1 gestrichen wurde.
// Falsch: feste id, kollidiert bei mehrfacher Verwendung
function Feld({ label }) {
return (
<>
<label htmlFor="feld">{label}</label>
<input id="feld" />
</>
);
}
// Richtig: useId erzeugt je Instanz eine eindeutige id
import { useId } from 'react';
function Feld({ label, hinweis }) {
const id = useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} aria-describedby={hinweis ? `${id}-hinweis` : undefined} />
{hinweis && <p id={`${id}-hinweis`}>{hinweis}</p>}
</>
);
}
Der Vorteil des abgeleiteten Suffixes (${id}-hinweis): Ein useId-Aufruf reicht für beliebig viele zusammengehörige Referenzen einer Komponente.
Fragmente statt Wrapper
Jede Komponente gibt genau einen Knoten zurück – der klassische Weg dorthin ist ein <div>. In den meisten Fällen ist das harmlos, in drei Fällen nicht:
- In Listen: Zwischen
<ul>und<li>darf keindivstehen. Der Screenreader meldet sonst keine Liste mehr, und die Ansage „Liste mit 7 Einträgen“ entfällt. - In Tabellen: Zwischen
<table>,<tbody>und<tr>gilt dasselbe – siehe Tabellen semantisch aufbauen. - In Definitionslisten zwischen
<dl>und<dt>/<dd>.
// Falsch: der Wrapper zerstört die Listensemantik
{eintraege.map((e) => (
<div key={e.id}>
<li>{e.text}</li>
</div>
))}
// Richtig: Fragment mit key
{eintraege.map((e) => (
<Fragment key={e.id}>
<li>{e.text}</li>
</Fragment>
))}
Wie sich Komponentenbäume sonst noch in Struktur ohne Bedeutung verwandeln, steht unter „div-soup“ vermeiden.
Fokus nach dem Routenwechsel
Der Befund, den kein Framework von selbst löst. Bei einem echten Seitenaufruf setzt der Browser den Fokus zurück und liest den neuen Titel; bei client-seitigem Routing passiert beides nicht.
function Seite({ titel, children }) {
const ueberschrift = useRef(null);
useEffect(() => {
document.title = `${titel} | Beispiel`;
ueberschrift.current?.focus();
}, [titel]);
return (
<>
{/* tabindex="-1" macht die Überschrift fokussierbar, ohne sie in die Tab-Reihenfolge zu legen */}
<h1 ref={ueberschrift} tabIndex={-1}>{titel}</h1>
{children}
</>
);
}
Zwei Details entscheiden über die Qualität: tabIndex={-1} hält die Überschrift aus der Tab-Reihenfolge heraus – sie soll fokussierbar sein, aber kein Stopp. Und der Fokusring darf hier ausnahmsweise unterdrückt werden, weil kein Bedienelement betroffen ist; überall sonst gilt 2.4.7.
Alternativ – und für viele Anwendungen angenehmer – kündigt eine Live-Region den Wechsel an, statt den Fokus zu bewegen. Wann welche Variante passt, steht unter Live-Regionen.
Dialoge: Portal ja, ARIA-Nachbau nein
createPortal löst das Layoutproblem – der Dialog wird ans Ende des <body> gerendert und liegt damit über allem. Was es nicht löst, ist die Zugänglichkeit: Fokusfalle, Escape, Fokusrückgabe und das Inertisieren des Hintergrunds bleiben deine Aufgabe.
Genau deshalb ist das native <dialog> mit showModal() auch in React die erste Wahl: Es bringt all das mit. Der komplette Aufbau steht unter Dialoge / Modals.
Linting: der billigste Prüfschritt
eslint-plugin-jsx-a11y prüft das JSX statisch und meldet unter anderem fehlende alt-Attribute, onClick an nicht interaktiven Elementen ohne Tastaturbedienung, ungültige ARIA-Attribute und aria-*-Werte, die es nicht gibt.
Zwei Einschränkungen gehören dazu. Erstens sieht der Linter nur statisches JSX – was zur Laufzeit entsteht, prüft er nicht. Zweitens ist er kein Ersatz für einen Testlauf: Er findet, was formal falsch ist, nicht was inhaltlich nicht trägt. Die Abgrenzung steht unter Prüfwerkzeuge im Vergleich.
Ergänzend lohnt axe-core/react oder jest-axe im Testlauf – und trotzdem ein manueller Durchgang mit NVDA vor jedem Release.
Formulare: kontrolliert, aber nicht angesagt
Kontrollierte Eingabefelder sind React-Standard – und sie sagen von sich aus gar nichts. Beim fehlerhaften Absenden fehlen typischerweise drei Dinge: die Verknüpfung der Fehlermeldung mit dem Feld, der Zustand aria-invalid und eine Zusammenfassung, die den Fokus bekommt.
function Formular() {
const [fehler, setFehler] = useState([]);
const zusammenfassung = useRef(null);
const id = useId();
function absenden(e) {
e.preventDefault();
const gefunden = pruefen();
setFehler(gefunden);
if (gefunden.length) {
// erst nach dem Rendern fokussieren
queueMicrotask(() => zusammenfassung.current?.focus());
}
}
return (
<form onSubmit={absenden} noValidate>
{fehler.length > 0 && (
<div ref={zusammenfassung} tabIndex={-1} role="alert">
<h2>{fehler.length} Angaben fehlen</h2>
<ul>
{fehler.map((f) => (
<li key={f.feld}><a href={`#${id}-${f.feld}`}>{f.text}</a></li>
))}
</ul>
</div>
)}
<label htmlFor={`${id}-email`}>E-Mail-Adresse</label>
<input
id={`${id}-email`}
type="email"
aria-invalid={fehler.some((f) => f.feld === 'email') || undefined}
aria-describedby={fehler.some((f) => f.feld === 'email') ? `${id}-email-fehler` : undefined}
/>
…
</form>
);
}
Drei Details sind entscheidend. noValidate schaltet die Browser-Blasen ab, die Screenreader unterschiedlich behandeln – die eigene Meldung ist verlässlicher. aria-invalid nur setzen, wenn es zutrifft (|| undefined), sonst steht aria-invalid="false" an jedem Feld und stumpft ab. Und die Zusammenfassung bekommt den Fokus, statt nur angesagt zu werden: Von dort führt ein Link direkt zum betroffenen Feld. Die Muster im Einzelnen stehen unter
Fehlermeldungen barrierefrei
und
Validierung & Pflichtfelder.
Server Components: wo die Fokusverwaltung hingehört
In Next.js und ähnlichen Setups laufen Komponenten standardmäßig auf dem Server. Für Hilfsmittel ändert das nichts – es kommt derselbe DOM an. Praktisch heißt es aber: Alles, was useRef, useEffect oder document braucht, gehört in eine Client-Komponente. Das betrifft genau die drei Dinge von dieser Seite – Fokus nach dem Routenwechsel, Titelsetzung und Live-Ankündigungen.
Ein verbreiteter Fehlgriff ist, deshalb die ganze Seite zur Client-Komponente zu machen. Der schlankere Weg: eine kleine Client-Komponente, die nichts rendert außer dem Fokusziel, und die serverseitig gerenderten Inhalte unangetastet lassen.
Was der Linter nicht sieht
eslint-plugin-jsx-a11y prüft statisches JSX. Vier Befundtypen entstehen erst zur Laufzeit und brauchen deshalb einen Testlauf:
- Doppelte IDs aus zweifach gerenderten Komponenten – nur im fertigen DOM sichtbar.
- Fokus, der ins Leere fällt, wenn ein Element beim Schließen entfernt wird.
- Nicht angesagte Änderungen – der Linter kennt keine Live-Regionen.
- Falsche Reihenfolge, wenn CSS die visuelle Anordnung vom DOM entkoppelt.
Für die ersten drei lohnt ein automatischer Testlauf mit jest-axe:
import { axe } from 'jest-axe';
test('Formular hat keine Verstöße', async () => {
const { container } = render(<Formular />);
expect(await axe(container)).toHaveNoViolations();
});
Das findet mechanische Fehler zuverlässig und die inhaltlichen gar nicht – ob ein Alt-Text die richtige Information trägt oder eine Ansage verständlich ist, bleibt manuell. Die Abgrenzung im Detail steht unter Prüfwerkzeuge im Vergleich, der Gesamtablauf unter Barrierefreiheit selbst testen.
Ein Hinweis zur Testebene: Komponententests laufen in Isolation. Fokusreihenfolge, Landmarken und doppelte IDs entstehen erst im Zusammenspiel – dafür braucht es einen Lauf gegen die fertige Seite, etwa mit @axe-core/playwright.
Häufiger Fehler in der Praxis
onClick auf einem div. Der Linter meldet es, und die übliche Reaktion ist, die Regel abzuschalten statt <button> zu schreiben. Die Entscheidungshilfe steht unter Buttons vs. Links.
dangerouslySetInnerHTML für fremdes Markup. Alles, was dort hineingeht, umgeht Linter, Typprüfung und Review. Wenn es sein muss: den Inhalt vorher validieren.
Bedingtes Rendern ohne Ankündigung. {fehler && <p>…</p>} blendet eine Fehlermeldung ein, die niemand hört. Sie gehört in eine Live-Region oder bekommt den Fokus – siehe Fehlermeldungen barrierefrei.
Die Komponentenbibliothek ungeprüft übernehmen. Zehn Minuten Screenreader-Test an einer Komponente beantworten mehr als jede Selbstauskunft im Readme.
Häufige Fragen
Ist React schlechter für Barrierefreiheit als serverseitig gerendertes HTML?
Nicht grundsätzlich – Hilfsmittel lesen den fertigen DOM. React verlangt aber Arbeit, die eine klassische Seite geschenkt bekommt: Titel, Fokus und Ankündigungen beim Routenwechsel.
Brauche ich role und aria-* in React häufiger als in HTML?
Nein, eher seltener, wenn du native Elemente nutzt. Die Attributschreibweise in JSX ist identisch (aria-label, role), nur class und for heißen anders.
Wie teste ich React-Komponenten automatisiert?
jest-axe oder @axe-core/playwright im Testlauf. Sie finden mechanische Fehler zuverlässig; Fokusreihenfolge und Ankündigungen bleiben manuell.
Was ist mit Next.js und React Server Components?
Für Hilfsmittel ändert sich nichts – es kommt derselbe DOM an. Was sich ändert, ist der Ort der Fokusverwaltung: Sie gehört in eine Client-Komponente, weil sie den Browser braucht.
Verwandte Themen
- Barrierefreiheit nach Framework – die vier Probleme, die überall gleich sind
- Barrierefreiheit in Vue – die Parallelseite mit anderen Fallstricken
- Die erste Regel von ARIA – warum der Elementtausch fast immer die kürzere Lösung ist
- Rollen, States & Properties – wenn ein Eigenbau doch nötig ist
Quellen
- React: useId – die offizielle Referenz zur kollisionsfreien ID-Erzeugung
- eslint-plugin-jsx-a11y – Regelsatz und Dokumentation
- ARIA Authoring Practices Guide – die Referenzmuster für Widgets (W3C)