Muster für Menüs, Tabs und Listen ohne Maus
Ein Dropdown-Menü, das nur mit der Maus bedienbar ist, oder eine Tab-Leiste, die bei jedem Tab-Druck durch zehn einzelne Reiter springt, verstößt gegen etablierte WAI-ARIA-Erwartungen. Tastaturnavigation in React folgt festen Mustern wie Roving Tabindex und Pfeiltasten-Handling, die sich in jeder komplexen Komponente wiederverwenden lassen.
Inhaltsverzeichnis
- 1. Warum Tab allein für komplexe Komponenten nicht reicht
- 2. Das Roving-Tabindex-Muster verstehen
- 3. Einen eigenen useRovingTabindex Hook bauen
- 4. Tastaturnavigation in Dropdown-Menüs
- 5. Tab-Leisten nach dem WAI-ARIA-Tabs-Muster
- 6. Comboboxen und Autocomplete mit Pfeiltasten
- 7. Type-Ahead-Suche in langen Listen
- 8. Tastaturnavigation automatisiert testen
- 9. Navigationsmuster im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Tab allein für komplexe Komponenten nicht reicht
Die Tab-Taste ist für die Navigation zwischen unabhängigen interaktiven Elementen einer Seite gedacht, nicht für die Navigation innerhalb eines einzelnen zusammengesetzten Widgets. Eine Tastaturnavigation in React, die jedes Element eines Dropdown-Menüs, jede Zelle einer Tabelle oder jeden Tab-Reiter einzeln in die Tab-Reihenfolge aufnimmt, zwingt Tastaturnutzer dazu, durch zehn oder zwanzig Tab-Drücke zu navigieren, nur um von einem Menüeintrag zum nächsten zu gelangen. Das widerspricht direkt den WAI-ARIA Authoring Practices, die für solche zusammengesetzten Widgets Pfeiltasten-Navigation innerhalb der Komponente vorsehen und Tab nur für den Ein- und Ausstieg aus der gesamten Komponente reservieren.
Das richtige Modell für Tastaturnavigation in React-Komponenten wie Menüs, Tab-Leisten, Radiogruppen und Listen sieht deshalb anders aus: Ein einziger Tab-Druck bringt den Fokus in die Komponente, danach übernehmen die Pfeiltasten die interne Navigation zwischen den Optionen, und ein weiterer Tab-Druck verlässt die Komponente wieder als Ganzes. Diese Struktur folgt exakt dem Verhalten nativer Betriebssystem-Widgets wie Dropdown-Menüs oder Radiobutton-Gruppen, die Nutzer aus jedem Betriebssystem bereits kennen.
Technisch wird dieses Muster durch Roving Tabindex umgesetzt: Zu jedem Zeitpunkt hat genau ein Element innerhalb der Komponente tabindex="0", alle anderen haben tabindex="-1". Pfeiltasten-Drücke verschieben diesen Zustand zwischen den Elementen, ohne die globale Tab-Reihenfolge der Seite zu beeinflussen. Das ist der zentrale Baustein für jede Form korrekter Tastaturnavigation in zusammengesetzten React-Komponenten.
// WRONG: every menu item is in the global tab order
function NaiveMenu({ items }) {
return (
<ul role="menu">
{items.map((item) => (
<li key={item.id} role="menuitem" tabIndex={0}>
{item.label}
</li>
))}
</ul>
);
// Ten menu items mean ten separate Tab presses just to reach
// the item after the menu. Arrow keys do nothing at all.
}
2. Das Roving-Tabindex-Muster verstehen
Roving Tabindex basiert auf einer einfachen Regel: Innerhalb einer Gruppe zusammengehöriger Elemente, etwa den Einträgen eines Menüs oder den Reitern einer Tab-Leiste, trägt immer nur ein Element tabindex="0", alle übrigen tragen tabindex="-1". Damit erscheint für den Browser nur ein einziges fokussierbares Element in der globalen Tab-Reihenfolge, während innerhalb der Gruppe per Pfeiltaste zwischen den Elementen gewechselt wird, indem der Tabindex-Wert programmatisch verschoben und der Fokus explizit per element.focus() gesetzt wird.
Ein entscheidender Aspekt bei der Umsetzung von Roving Tabindex in React: Der aktuell aktive Index sollte im Komponenten-State gehalten werden, nicht direkt im DOM abgefragt werden. Bei jedem Pfeiltasten-Druck wird der Index berechnet, der neue Tabindex-Wert entsprechend gesetzt, und der Fokus per useEffect oder direkt im Event-Handler auf das neue aktive Element verschoben. Diese Trennung zwischen React-State und DOM-Fokus ist der Grund, warum sich das Muster gut in funktionale Komponenten integrieren lässt, ohne auf manuelles DOM-Querying angewiesen zu sein.
3. Einen eigenen useRovingTabindex Hook bauen
Wie beim Focus Trap lohnt sich auch für Roving Tabindex ein wiederverwendbarer Hook, der die Logik für beliebige listenartige Komponenten kapselt. Der Hook nimmt die Anzahl der Elemente sowie optionale Konfiguration für die Navigationsrichtung entgegen (horizontal für Tab-Leisten, vertikal für Menüs, beide Richtungen für Grids) und liefert den aktuell aktiven Index sowie eine Funktion zur Behandlung von Tastatur-Events zurück.
Wichtig ist die Unterstützung von Home- und End-Tasten, die zum ersten beziehungsweise letzten Element springen, sowie das Umbiegen am Rand der Liste, sodass Pfeil-Runter am letzten Element wieder zum ersten Element zurückspringt. Diese Details entsprechen exakt dem Verhalten, das die WAI-ARIA Authoring Practices für Menü- und Listbox-Muster vorschreiben, und werden von erfahrenen Tastaturnutzern selbstverständlich erwartet.
import { useState, useCallback } from 'react';
function useRovingTabindex(itemCount, orientation = 'vertical') {
const [activeIndex, setActiveIndex] = useState(0);
const handleKeyDown = useCallback(
(event) => {
const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
const prevKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
if (event.key === nextKey) {
event.preventDefault();
setActiveIndex((i) => (i + 1) % itemCount);
} else if (event.key === prevKey) {
event.preventDefault();
setActiveIndex((i) => (i - 1 + itemCount) % itemCount);
} else if (event.key === 'Home') {
event.preventDefault();
setActiveIndex(0);
} else if (event.key === 'End') {
event.preventDefault();
setActiveIndex(itemCount - 1);
}
},
[itemCount, orientation]
);
const getItemProps = (index) => ({
tabIndex: index === activeIndex ? 0 : -1,
onKeyDown: handleKeyDown,
'data-active': index === activeIndex,
});
return { activeIndex, setActiveIndex, getItemProps };
}
export default useRovingTabindex;
4. Tastaturnavigation in Dropdown-Menüs
Ein Dropdown-Menü kombiniert mehrere Anforderungen an Tastaturnavigation in React gleichzeitig: Öffnen mit Enter, Leertaste oder Pfeil-Runter auf dem Trigger-Button, vertikale Navigation zwischen Menüeinträgen mit den Pfeiltasten, Auswahl mit Enter, und Schließen mit Escape bei gleichzeitiger Fokus-Rückgabe auf den Trigger-Button. Diese Kombination folgt direkt dem WAI-ARIA-Menü-Muster und lässt sich mit dem zuvor gebauten useRovingTabindex Hook für den Navigationsteil umsetzen, ergänzt um Escape-Handling ähnlich dem Focus-Trap-Muster.
Ein Detail, das häufig übersehen wird: Beim Öffnen des Menüs per Pfeil-Runter sollte der Fokus direkt auf den ersten Menüeintrag springen, nicht nur das Menü sichtbar machen, ohne den Fokus zu verschieben. Wird das Menü hingegen per Pfeil-Hoch geöffnet, erwarten viele Nutzer, direkt auf dem letzten Eintrag zu landen, ein Detail, das aus der Desktop-Softwarewelt stammt und von aufmerksamen Tastaturnutzern registriert wird.
5. Tab-Leisten nach dem WAI-ARIA-Tabs-Muster
Bei Tab-Leisten gibt es eine wichtige Designentscheidung: automatische Aktivierung versus manuelle Aktivierung. Bei automatischer Aktivierung wechselt der angezeigte Panel-Inhalt sofort bei jedem Pfeiltasten-Druck mit, bei manueller Aktivierung navigiert die Pfeiltaste nur zwischen den Reitern, und erst Enter oder Leertaste aktiviert den fokussierten Reiter. Für Tab-Leisten mit performanceintensivem Inhalt, etwa jedem Reiter zugeordnete Datenabfragen, ist manuelle Aktivierung meist die bessere Wahl, weil sie unnötige Datenabfragen bei schnellem Durchtippen durch die Reiter vermeidet.
Die Tastaturnavigation selbst folgt demselben Roving-Tabindex-Muster wie Menüs, mit horizontaler statt vertikaler Pfeiltasten-Richtung. Zusätzlich muss der aktive Tab-Reiter über aria-selected="true" markiert werden, und das zugehörige Panel braucht role="tabpanel" mit einer Referenz auf den zugehörigen Tab per aria-labelledby. Ohne diese Verknüpfung bleibt für Screenreader-Nutzer unklar, welcher Inhalt zu welchem Reiter gehört, selbst wenn die reine Tastaturnavigation bereits korrekt funktioniert.
function Tabs({ tabs, activeIndex, onSelect, getItemProps }) {
return (
<div role="tablist" aria-label="Settings sections">
{tabs.map((tab, index) => (
<button
key={tab.id}
role="tab"
id={`tab-${tab.id}`}
aria-selected={index === activeIndex}
aria-controls={`panel-${tab.id}`}
onClick={() => onSelect(index)}
{...getItemProps(index)}
>
{tab.label}
</button>
))}
</div>
);
}
6. Comboboxen und Autocomplete mit Pfeiltasten
Comboboxen sind die anspruchsvollste Komponente für Tastaturnavigation in React, weil sie ein Textfeld mit einer Popup-Liste kombinieren. Pfeil-Runter im Textfeld öffnet die Vorschlagsliste und bewegt den aktiven Vorschlag, ohne den Fokus tatsächlich vom Eingabefeld wegzubewegen, das Textfeld behält den DOM-Fokus, während der aktive Zustand der Liste über aria-activedescendant kommuniziert wird, statt Roving Tabindex zu nutzen. Dieser Unterschied zu Menüs und Tabs ist bewusst, weil der Nutzer weiterhin tippen können muss, während er durch die Vorschläge navigiert.
aria-activedescendant verweist auf die ID des aktuell aktiven Listeneintrags, während der Fokus selbst auf dem Eingabefeld verbleibt. Screenreader kündigen dadurch den aktiven Vorschlag an, ohne dass ein tatsächlicher Fokuswechsel im DOM stattfindet. Enter übernimmt den aktiven Vorschlag ins Textfeld, Escape schließt die Liste ohne Übernahme, und Pfeil-Hoch am ersten Eintrag verhält sich je nach Implementierung entweder umlaufend oder deaktiviert die Listen-Auswahl wieder zugunsten des freien Texteingabemodus.
7. Type-Ahead-Suche in langen Listen
Für lange Listen, etwa eine Länderauswahl mit 200 Einträgen, reicht reine Pfeiltasten-Navigation nicht aus, hier erwarten Nutzer Type-Ahead, also das Springen zum nächsten Eintrag, der mit dem zuletzt getippten Buchstaben beginnt. Das Muster sammelt aufeinanderfolgende Tastendrücke innerhalb eines kurzen Zeitfensters, meist unter einer Sekunde, zu einem Suchbegriff und springt zum ersten passenden Element, ähnlich wie es aus nativen Dateiauswahl-Dialogen bekannt ist.
Die Implementierung erfordert einen kleinen internen Zustand für den aktuell aufgebauten Suchbegriff sowie einen Timer, der den Suchbegriff nach Ablauf des Zeitfensters zurücksetzt. Wird derselbe Buchstabe mehrfach hintereinander gedrückt, ohne dass das Zeitfenster abläuft, springt die Implementierung meist zyklisch zum nächsten Eintrag, der mit diesem Buchstaben beginnt, statt bei jedem Tastendruck neu von vorne zu suchen. Dieses Detail unterscheidet eine ausgereifte Tastaturnavigation von einer oberflächlichen Nachahmung des Musters.
8. Tastaturnavigation automatisiert testen
Automatisierte Tests für Tastaturnavigation in React simulieren Pfeiltasten-Drücke mit @testing-library/user-event und prüfen, ob sich der Fokus wie erwartet zwischen den Elementen bewegt. Wichtig ist, sowohl das Umlaufen am Rand der Liste als auch die Home- und End-Tasten explizit zu testen, da diese Randfälle in der Praxis am häufigsten übersehen werden, wenn ein Entwickler die Komponente nur mit wenigen Testklicks statt echter Tastaturnavigation prüft.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Menu from './Menu';
test('arrow keys move focus and wrap at the edges', async () => {
const user = userEvent.setup();
render(<Menu items={['First', 'Second', 'Third']} />);
const first = screen.getByText('First');
first.focus();
await user.keyboard('{ArrowDown}');
expect(screen.getByText('Second')).toHaveFocus();
await user.keyboard('{ArrowDown}{ArrowDown}');
expect(screen.getByText('First')).toHaveFocus(); // wraps around
await user.keyboard('{End}');
expect(screen.getByText('Third')).toHaveFocus();
});
9. Navigationsmuster im direkten Vergleich
Je nach Komponententyp unterscheidet sich das passende Tastaturnavigation-Muster in React deutlich. Die folgende Übersicht ordnet die wichtigsten Widget-Typen den jeweils empfohlenen Mustern zu.
| Komponente | Navigationsmuster | Fokus-Strategie | Zusatz |
|---|---|---|---|
| Dropdown-Menü | Vertikal, Roving Tabindex | DOM-Fokus wandert | Escape schließt und gibt Fokus zurück |
| Tab-Leiste | Horizontal, Roving Tabindex | DOM-Fokus wandert | Manuelle Aktivierung bei teurem Inhalt |
| Combobox / Autocomplete | Vertikal, aria-activedescendant | Fokus bleibt im Textfeld | Tippen bleibt jederzeit möglich |
| Lange Liste (Type-Ahead) | Vertikal + Buchstaben-Suche | DOM-Fokus wandert | Zeitfenster für Type-Ahead nötig |
| Daten-Grid | Zweidimensional (beide Achsen) | DOM-Fokus wandert | role=grid statt role=table |
Die Wahl zwischen Roving Tabindex und aria-activedescendant ist keine Geschmacksfrage, sondern hängt direkt davon ab, ob der Nutzer während der Navigation weiterhin Text eingeben können muss. Diese Unterscheidung ist der häufigste Grund, warum selbstgebaute Comboboxen an der Tastaturnavigation scheitern, wenn Entwickler das Menü-Muster unreflektiert auf Autocomplete-Felder übertragen.
Mironsoft
React-Entwicklung mit Fokus auf Barrierefreiheit und Design-Systeme
Menüs und Tabs, die auch ohne Maus funktionieren?
Wir implementieren Roving Tabindex, Pfeiltasten-Handler und WAI-ARIA-konforme Muster für Menüs, Tabs, Comboboxen und Grids in euren React-Komponenten.
Komponenten-Audit
Bestehende Menüs, Tabs und Listen auf korrekte Tastaturnavigation prüfen
Hook-Bibliothek
Wiederverwendbare Roving-Tabindex- und Type-Ahead-Hooks für euer Design-System
Testautomatisierung
Tastaturtests mit user-event in die bestehende Testsuite integrieren
10. Zusammenfassung
Solide Tastaturnavigation in React unterscheidet konsequent zwischen der globalen Tab-Reihenfolge einer Seite und der internen Navigation innerhalb zusammengesetzter Widgets. Roving Tabindex sorgt dafür, dass Menüs, Tab-Leisten und Listen jeweils nur einen einzigen Eintrag in der globalen Tab-Reihenfolge belegen, während Pfeiltasten die Bewegung innerhalb der Komponente übernehmen. Comboboxen bilden mit aria-activedescendant eine bewusste Ausnahme, weil der Nutzer während der Navigation weiterhin tippen können muss.
Type-Ahead-Suche ergänzt lange Listen um eine zusätzliche, buchstabenbasierte Navigationsebene, die aus nativen Betriebssystem-Widgets vertraut ist. Automatisierte Tests mit user-event stellen sicher, dass Umlaufverhalten, Home- und End-Tasten sowie die korrekte Fokus-Bewegung bei künftigen Änderungen erhalten bleiben, ohne bei jedem Refactoring erneut manuell mit der Tastatur geprüft werden zu müssen.
Tastaturnavigation in React — Das Wichtigste auf einen Blick
Roving Tabindex
Nur ein Element trägt tabindex="0", Pfeiltasten verschieben den aktiven Index innerhalb der Gruppe.
Menüs und Tabs
Ein Tab-Druck rein, Pfeiltasten navigieren intern, ein Tab-Druck wieder raus, entspricht nativen Betriebssystem-Mustern.
Comboboxen
aria-activedescendant statt Roving Tabindex, weil der Fokus im Textfeld bleiben muss.
Testing
Pfeiltasten, Umlaufverhalten sowie Home und End explizit mit user-event automatisiert prüfen.