Barrierefreie React-Komponenten von Grund auf entwickeln
Wie man wirklich barrierefreie React-Komponenten entwickelt — mit ARIA-Attributen, Tastaturnavigation, Fokus-Management und Testmustern für Screenreader.

Barrierefreiheit ist kein nachträglicher Gedanke und kein Compliance-Häkchen. Sie ist eine zentrale technische Anforderung. Unzugängliche Komponenten schließen Nutzer aus, die auf Screenreader, Tastaturnavigation oder alternative Eingabegeräte angewiesen sind. Barrierefreie React-Komponenten von Anfang an zu entwickeln ist einfacher, als sie später nachträglich zugänglich zu machen.
Die meisten Barrierefreiheits-Fehler fallen in wenige Kategorien: fehlende Beschriftungen, kaputte Tastaturnavigation und falsche ARIA-Verwendung. Wer diese Muster behebt, deckt den Großteil der Probleme ab.
Semantisches HTML zuerst
Die einfachste Maßnahme für Barrierefreiheit ist das richtige HTML-Element. Native Elemente bringen Tastaturbedienung, Fokus-Management und Screenreader-Ansagen bereits mit.
// ❌ Div pretending to be a button — no keyboard support, no role
function BadButton({ onClick, children }: { onClick: () => void; children: React.ReactNode }) {
return (
<div className="btn" onClick={onClick}>
{children}
</div>
);
}
// Screen reader sees: generic container
// Keyboard user: cannot focus or activate with Enter/Space// ✅ Actual button element — keyboard, focus, and role built-in
function GoodButton({ onClick, children }: { onClick: () => void; children: React.ReactNode }) {
return (
<button type="button" className="btn" onClick={onClick}>
{children}
</button>
);
}
// Screen reader sees: "Submit, button"
// Keyboard user: Tab to focus, Enter or Space to activateZu bevorzugende semantische Elemente: <button> statt <div onClick>, <a href> statt <span onClick>, <nav> statt <div class="nav">, <main> statt <div id="content">, <dialog> statt <div class="modal">.
Beschriftung interaktiver Elemente
Jedes interaktive Element braucht einen zugänglichen Namen. Ohne ihn geben Screenreader die Rolle des Elements an, aber nicht seinen Zweck.
// ❌ Icon button with no accessible name
function IconButton({ icon, onClick }: { icon: string; onClick: () => void }) {
return (
<button onClick={onClick}>
<svg aria-hidden="true">{/* icon SVG */}</svg>
</button>
);
}
// Screen reader announces: "button" — which button?// ✅ Icon button with aria-label
function IconButton({
icon,
label,
onClick,
}: {
icon: string;
label: string;
onClick: () => void;
}) {
return (
<button onClick={onClick} aria-label={label}>
<svg aria-hidden="true">{/* icon SVG */}</svg>
</button>
);
}
// Usage:
<IconButton icon="trash" label="Delete item" onClick={handleDelete} />
// Screen reader announces: "Delete item, button"Bei Formularfeldern immer <label>-Elemente verwenden, deren htmlFor auf die id des Felds zeigt. Das Attribut aria-label ist für Fälle gedacht, in denen es keine sichtbare Beschriftung gibt, etwa bei Schaltflächen, die nur aus einem Icon bestehen.
Tastaturnavigation in eigenen Komponenten
Eigene Dropdowns, Menüs und Tabs brauchen eine Tastaturunterstützung, die ihre nativen Gegenstücke nachbildet. Die WAI-ARIA Authoring Practices definieren die erwarteten Tastaturinteraktionen.
import { useState, useRef, useCallback } from 'react';
interface DropdownProps {
label: string;
options: { value: string; label: string }[];
value: string;
onChange: (value: string) => void;
}
function Dropdown({ label, options, value, onChange }: DropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
const listRef = useRef<HTMLUListElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const selectedOption = options.find(o => o.value === value);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
setActiveIndex(0);
} else {
setActiveIndex(i => Math.min(i + 1, options.length - 1));
}
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex(i => Math.max(i - 1, 0));
break;
case 'Enter':
case ' ':
e.preventDefault();
if (isOpen && activeIndex >= 0) {
onChange(options[activeIndex].value);
setIsOpen(false);
buttonRef.current?.focus();
} else {
setIsOpen(true);
setActiveIndex(0);
}
break;
case 'Escape':
setIsOpen(false);
buttonRef.current?.focus();
break;
}
},
[isOpen, activeIndex, options, onChange]
);
return (
<div onKeyDown={handleKeyDown}>
<button
ref={buttonRef}
aria-haspopup="listbox"
aria-expanded={isOpen}
aria-label={label}
onClick={() => setIsOpen(o => !o)}
>
{selectedOption?.label ?? 'Select...'}
</button>
{isOpen && (
<ul
ref={listRef}
role="listbox"
aria-label={label}
aria-activedescendant={
activeIndex >= 0 ? `option-${activeIndex}` : undefined
}
>
{options.map((option, index) => (
<li
key={option.value}
id={`option-${index}`}
role="option"
aria-selected={option.value === value}
data-active={index === activeIndex}
onClick={() => {
onChange(option.value);
setIsOpen(false);
buttonRef.current?.focus();
}}
>
{option.label}
</li>
))}
</ul>
)}
</div>
);
}Zentrale Tastaturmuster: Die Pfeiltasten navigieren durch die Optionen. Enter/Leertaste wählt aus. Escape schließt und setzt den Fokus zurück auf den Auslöser. aria-activedescendant teilt Screenreadern mit, welche Option gerade hervorgehoben ist.
Fokus-Management in Modalen
Modale müssen den Fokus einschließen — Tab sollte durch die fokussierbaren Elemente innerhalb des Modals wechseln, nicht auf die Seite dahinter entweichen.
import { useEffect, useRef } from 'react';
function Modal({
isOpen,
onClose,
title,
children,
}: {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}) {
const modalRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
// Save the element that had focus before modal opened
previousFocusRef.current = document.activeElement as HTMLElement;
// Focus the modal container
modalRef.current?.focus();
return () => {
// Restore focus when modal closes
previousFocusRef.current?.focus();
};
}
}, [isOpen]);
useEffect(() => {
if (!isOpen) return;
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onClose();
return;
}
if (e.key !== 'Tab') return;
const modal = modalRef.current;
if (!modal) return;
const focusable = modal.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-label={title}
tabIndex={-1}
onClick={e => e.stopPropagation()}
>
<h2>{title}</h2>
{children}
<button onClick={onClose}>Close</button>
</div>
</div>
);
}Drei Fokus-Anforderungen: den Fokus beim Öffnen des Modals einfangen, den Fokus innerhalb des Modals einschließen und den Fokus beim Schließen auf den Auslöser zurücksetzen.
Live-Regionen für dynamische Inhalte
Wenn sich Inhalte ohne Seitennavigation aktualisieren — etwa eine Toast-Benachrichtigung, ein Formular-Validierungsfehler oder ein Ladestatus — müssen Screenreader über die Änderung informiert werden.
// ❌ Dynamic error message — screen reader never announces it
function Form() {
const [error, setError] = useState('');
return (
<form>
<input type="email" />
{error && <span className="error">{error}</span>}
</form>
);
}// ✅ Live region announces the error to screen readers
function Form() {
const [error, setError] = useState('');
return (
<form>
<input type="email" aria-describedby="email-error" />
<span
id="email-error"
role="alert"
aria-live="assertive"
className="error"
>
{error}
</span>
</form>
);
}
// When error text changes, screen reader interrupts to announce itaria-live="assertive" unterbricht die aktuelle Ansage — für Fehler verwenden. aria-live="polite" wartet, bis der Screenreader seine aktuelle Ausgabe beendet hat — für Statusmeldungen wie „3 Ergebnisse gefunden" verwenden.
Die wichtigsten Erkenntnisse
- Semantisches HTML verwenden, bevor man zu ARIA greift — native Elemente übernehmen Tastatur, Fokus und Rollen automatisch
- Jedes interaktive Element beschriften — Icon-Schaltflächen brauchen
aria-label, Formularfelder brauchen<label>-Elemente - Tastaturnavigation implementieren für eigene Komponenten nach den WAI-ARIA-Mustern — Pfeiltasten, Enter, Escape
- Fokus in Modalen einschließen — vorherigen Fokus speichern, Tab innerhalb des Modals zykeln lassen, Fokus beim Schließen wiederherstellen
- Live-Regionen für dynamische Inhalte verwenden —
role="alert"für Fehler,aria-live="polite"für Statusmeldungen - Mit einem Screenreader testen — VoiceOver auf dem Mac, NVDA unter Windows oder ChromeVox in Chrome


