Tastaturnavigation in React: Muster für komplexe UI-Komponenten
AI generated
</>
{ }
React · Accessibility · Tastatur · Barrierefreiheit
Tastaturnavigation in React
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.

18 Min. Lesezeit Roving Tabindex · Pfeiltasten · WAI-ARIA React 18/19 · Menüs · Tabs · Comboboxen

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;

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.

11. FAQ: Tastaturnavigation in React

1Was ist Roving Tabindex?
Nur ein Element in der Gruppe trägt tabindex=0, Pfeiltasten verschieben den aktiven Index intern.
2Warum nicht jedes Menüelement per Tab?
Zwingt zu vielen Tab-Drücken. WAI-ARIA sieht Pfeiltasten-Navigation innerhalb des Widgets vor.
3Wann aria-activedescendant nutzen?
Bei Comboboxen, weil der Fokus im Textfeld bleiben muss, damit weiter getippt werden kann.
4Was ist Type-Ahead-Suche?
Buchstaben-basiertes Springen zu Einträgen in langen Listen innerhalb eines kurzen Zeitfensters.
5Tab-Leisten automatisch oder manuell aktivieren?
Manuell bei teurem Panel-Inhalt, um unnötige Datenabfragen zu vermeiden.
6Wie navigiere ich zweidimensional im Grid?
role=grid mit Roving Tabindex auf beiden Achsen, Pfeiltasten für Spalten und Zeilen.
7Wie automatisiert testen?
user-event simuliert Pfeiltasten und Home/End, toHaveFocus() prüft das Ergebnis.
8Was passiert am Listenrand?
Üblicherweise Umlaufen: letztes Element springt zum ersten und umgekehrt.
9Muss ich Home und End implementieren?
Ja bei längeren Listen, erfahrene Nutzer erwarten diese Tasten aus nativen Widgets.
10Reicht CSS für sichtbaren Fokus?
Ein deutlicher :focus-visible Stil ist zusätzlich zur Tabindex-Logik notwendig.