So fügen Sie Tastaturkürzel zu einer Web-App hinzu
So fügen Sie einer Web-App Tastenkürzel mit document-level keydown-Listenern, Schutz beim Tippen, Mac- und Windows-Modifikatoren, Sequenzen und React-Cleanup hinzu.
Um Tastaturkürzel zu einer Web-App hinzuzufügen, hängen Sie einen einzelnen keydown-Listener an document an, gleichen Sie event.key zusammen mit den Modifier-Booleans ab, überspringen Sie das Event, wenn dessen Target ein editierbares Element ist, und entfernen Sie den Listener mit derselben Funktionsreferenz, wenn die zugehörige Komponente unmountet wird.
Das erste Shortcut ist meist schnell geschrieben. Die Probleme kommen in der Regel später: Jemand tippt „k” in ein Suchfeld und die Befehlspalette öffnet sich, oder ein Kollege am Mac stellt fest, dass das Shortcut überhaupt nichts bewirkt.
Dieser Artikel geht von diesem naiven Listener aus und behebt jeden Fehlerfall der Reihe nach: Auslösen während der Eingabe, Mac- versus Windows-Modifier, Zwei-Tasten-Sequenzen, Listener-Leaks in React sowie die Accessibility-Regeln, die speziell für Shortcuts gelten. Die meisten Korrekturen sind wenige Zeilen TypeScript, die Sie in einen bestehenden Handler einfügen können.
Die wichtigsten Erkenntnisse
- Ein
keydown-Listener aufdocumentempfängt jeden Tastenanschlag auf der Seite, daher muss der Handler frühzeitig zurückkehren, wennevent.targeteininput,textarea,selectoder ein beliebiges Element ist, dessenisContentEditabletrue ist. - Ein Shortcut, das nur
event.ctrlKeyprüft, wird auf einem Mac nie ausgelöst, da die Command-Tasteevent.metaKeysetzt; prüfen Sieevent.metaKey || event.ctrlKey, damit ein einziges Binding beide Plattformen abdeckt. - Für das Matching ist keine Plattformerkennung nötig; ermitteln Sie die Plattform nur für die Anzeige – das ist die einzige Verwendung, die MDN für
navigator.platformdokumentiert. - Eine Sequenz wie
ggefolgt vonibenötigt einen Buffer, ein Ablauf-Timeout, einen Reset bei jeder Nicht-Präfix-Taste und eine erneute Prüfung dieser Taste als Beginn einer neuen Sequenz. - In React registrieren Sie den Listener in
useEffect, entfernen dieselbe Referenz im Cleanup und memoisieren den Handler mituseCallback, wenn er Props oder State liest.
Der naive JavaScript-Listener für Tastaturkürzel
Das einfachste funktionierende Shortcut ist ein keydown-Listener, der event.key mit einem Zeichen vergleicht und bei einem Treffer preventDefault() aufruft. Verwenden Sie event.key, niemals das veraltete keyCode.
document.addEventListener('keydown', (event) => {
if (event.ctrlKey && event.key.toLowerCase() === 'k') {
event.preventDefault();
openCommandPalette();
}
});
Die Kleinschreibung von event.key sorgt dafür, dass der Abgleich Caps Lock und Shift übersteht. Alles Übrige an diesem Listener ist ein Bug, der nur auf einen Nutzer wartet.
Wie verhindert man, dass Shortcuts während der Eingabe ausgelöst werden?
Ein Shortcut-Handler muss event.target prüfen, bevor er irgendetwas tut, denn ein Listener auf Dokumentebene empfängt auch die Tastenanschläge, die ein Nutzer in ein Suchfeld tippt. Der übliche Filter prüft drei Tag-Namen – und genau darin liegt die Lücke: Ein contenteditable-Bereich behält sein eigenes Tag (meist div), sodass ein Rich-Text-Editor die Prüfung passiert und das Shortcut mitten im Satz feuert. Session Replays von Apps mit globalen Shortcuts zeigen genau das: Ein Nutzer tippt in ein Feld, und die Seite navigiert bei dem Buchstaben weg, der zufällig belegt war.
Verwenden Sie stattdessen isContentEditable. Es ist true für jedes Element, das der Nutzer bearbeiten kann, einschließlich solcher, die die Editierbarkeit von einem Vorfahren erben:
function isTyping(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false;
const tag = target.tagName;
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'
|| target.isContentEditable;
}
document.addEventListener('keydown', (event) => {
if (isTyping(event.target)) return;
// matching below
});
Kehren Sie am Anfang des Handlers zurück, damit nichts Nachgelagertes – einschließlich des Sequenz-Buffers in einem späteren Abschnitt – jemals einen getippten Tastenanschlag zu sehen bekommt.
Wie behandelt man Modifier-Tasten unter Mac und Windows?
Ein Shortcut, das nur event.ctrlKey prüft, ist auf einem Mac wirkungslos, da die Command-Taste event.metaKey setzt. Akzeptieren Sie beim Abgleich beide Modifier:
const mod = event.metaKey || event.ctrlKey;
if (mod && event.key.toLowerCase() === 'k') { /* ... */ }
Das matcht geringfügig zu breit (Ctrl+K funktioniert auch auf einem Mac), was harmlos ist. Vermieden wird dadurch die Plattformerkennung im Matching-Pfad. navigator.platform ist als unzuverlässig für Erkennungszwecke dokumentiert, und die einzige von MDN befürwortete Verwendung ist die Wahl zwischen ⌘ und Ctrl, wenn ein Shortcut dem Nutzer angezeigt wird. Belassen Sie es dabei:
| Physische Taste | Event-Eigenschaft | Anzeige |
|---|---|---|
| Command (macOS) | metaKey | ⌘ |
| Control (Windows/Linux) | ctrlKey | Ctrl |
| Windows-Taste | metaKey | Nicht belegen |
const isMac = navigator.platform.startsWith('Mac') || navigator.platform === 'iPhone';
const formatKeys = (keys: string[]) =>
keys.map((k) => (k === 'mod' ? (isMac ? '⌘' : 'Ctrl') : k)).join(isMac ? '' : '+');
Wie unterstützt man Tastensequenzen wie g gefolgt von i?
Eine Zwei-Tasten-Sequenz benötigt einen Buffer, ein Timeout, das ihn leert, einen Reset, sobald der Buffer kein gültiges Präfix mehr ist, und eine erneute Prüfung der auslösenden Taste als Beginn einer neuen Sequenz. Lässt man diese Taste fallen, muss der Nutzer sie zweimal drücken. Zwei weitere Regeln: Ignorieren Sie keydown-Events, bei denen event.repeat true ist, damit eine gehaltene Taste den Buffer nicht überflutet, und ignorieren Sie keydown-Events reiner Modifier-Tasten (Shift, Control, Meta, Alt, AltGraph) – sonst bricht das Drücken von Shift vor einem Akkord jede laufende Sequenz ab.
| Buffer | Ergebnis nach Hinzufügen der Taste | Aktion |
|---|---|---|
| beliebig | entspricht einem Binding | ausführen, Buffer leeren |
| beliebig | Präfix eines Bindings | Buffer behalten, Timeout neu starten |
| Länge > 1 | keine Übereinstimmung | Buffer leeren, Taste erneut allein einspeisen |
| Länge 1 | keine Übereinstimmung | Buffer leeren |
| beliebig | Timeout läuft ab | Buffer leeren |
type Binding = { keys: string[]; description: string; run: () => void };
const bindings: Binding[] = [
{ keys: ['g', 'i'], description: 'Go to inbox', run: () => navigate('/inbox') },
{ keys: ['g', 'p'], description: 'Go to projects', run: () => navigate('/projects') },
];
const MODIFIERS = new Set(['Control', 'Meta', 'Shift', 'Alt', 'AltGraph']);
let buffer: string[] = [];
let timer: ReturnType<typeof setTimeout> | undefined;
function reset() { buffer = []; clearTimeout(timer); }
function feed(key: string) {
buffer.push(key);
const exact = bindings.find(
(b) => b.keys.length === buffer.length && b.keys.every((k, i) => k === buffer[i]),
);
if (exact) { exact.run(); reset(); return; }
if (bindings.some((b) => buffer.every((k, i) => b.keys[i] === k))) {
clearTimeout(timer);
timer = setTimeout(reset, 800);
return;
}
const retry = buffer.length > 1;
reset();
if (retry) feed(key);
}
document.addEventListener('keydown', (event) => {
if (isTyping(event.target) || event.repeat || MODIFIERS.has(event.key)) return;
if (event.metaKey || event.ctrlKey || event.altKey) return; // chords go elsewhere
feed(event.key.toLowerCase());
});
Das 800-ms-Fenster ist eine Entscheidung, keine Messung; einige hundert Millisekunden sind üblich.
Wie registriert und bereinigt man einen Shortcut-Listener in React?
Fügen Sie in React den Listener innerhalb von useEffect hinzu und entfernen Sie dieselbe Funktionsreferenz im zugehörigen Cleanup; liest der Handler Props oder State, memoisieren Sie ihn mit useCallback und führen Sie ihn im Dependency-Array des Effects auf. Ohne das Cleanup stapelt jedes erneute Mounten einen weiteren Listener, und ein einziger Tastendruck führt die Aktion zweimal aus.
function useShortcuts(bindings: Binding[]) {
const handleKeyDown = useCallback((event: KeyboardEvent) => {
if (isTyping(event.target)) return;
// match against bindings here
}, [bindings]);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);
}
Achten Sie in diesem Hook auf die bindings-Dependency. Übergibt ein Aufrufer ein inline notiertes Array-Literal, ist es bei jedem Render ein anderes Array, wodurch handleKeyDown seine Identität ändert und der Effect den Listener jedes Mal entfernt und neu hinzufügt. Nichts geht kaputt, aber dieses Hin und Her ist vergeudete Arbeit. Deklarieren Sie das Array auf Modulebene oder kapseln Sie es in der aufrufenden Komponente in useMemo.
Seit React 18 durchläuft im Strict Mode jeder Effect in der Entwicklung eine zusätzliche Runde aus Setup und Teardown, sodass ein Cleanup, das eine andere Referenz entfernt als die hinzugefügte, sich sofort als doppelt ausgeführter Handler bemerkbar macht. Außerhalb von React gilt dieselbe Regel: ein addEventListener, ein passendes removeEventListener, dieselbe Funktion.
Shortcuts zugänglich und auffindbar halten
Drei Regeln gelten speziell für Shortcuts. Belegen Sie keine Kombinationen, die der Browser reserviert, darunter Cmd/Ctrl+W, Cmd/Ctrl+N, Cmd/Ctrl+T und Tab, und rufen Sie kein preventDefault() bei nativen Bearbeitungsakkorden wie Cmd/Ctrl+C auf. Machen Sie ein Shortcut niemals zum einzigen Weg zu einer Funktion; für dieselbe Aktion muss ein Menüeintrag oder eine Schaltfläche existieren. Und für Bindings mit einzelnen Zeichen verlangt WCAG 2.1 SC 2.1.4 Character Key Shortcuts (Level A) eines von drei Dingen: eine Möglichkeit, das Shortcut abzuschalten, eine Möglichkeit, es so neu zu belegen, dass es eine Taste wie Ctrl oder Alt enthält, oder einen so eng gefassten Geltungsbereich, dass es nur auslöst, während die eigene Komponente den Fokus hält.
Für die Auffindbarkeit belegen Sie ? mit einem Hilfedialog, der dasselbe bindings-Array rendert, das auch der Matcher verwendet. Gleichen Sie auf event.key === '?' ab statt auf Shift plus Schrägstrich-Taste, damit es auch auf Layouts funktioniert, bei denen ? auf einer anderen physischen Taste liegt.
if (event.key === '?' && !isTyping(event.target)) {
event.preventDefault();
helpDialog.showModal();
}
// inside the dialog
{bindings.map((b) => (
<li key={b.keys.join(' ')}><kbd>{formatKeys(b.keys)}</kbd> {b.description}</li>
))}
showModal() auf einem nativen <dialog> liefert Ihnen die Escape-Behandlung kostenlos; zur Fokusverwaltung innerhalb des Dialogs siehe den Leitfaden zu häufigen Accessibility-Problemen bei Modals.
Wann sollte man zu einer Shortcut-Bibliothek greifen?
Sobald Sie mehr als ein paar Bindings haben, lohnt es sich, Scoping, Konflikterkennung und Sequenzverarbeitung zu delegieren. TanStack Hotkeys ist eine Option: Eine Mod-Taste in einem Binding wird auf einem Mac zu Command und überall sonst zu Control aufgelöst, und Tastenanschläge, die für fokussierte Eingabeelemente bestimmt sind, werden automatisch übersprungen. Die Übersichtsseite kennzeichnet die Bibliothek weiterhin als Alpha und weist darauf hin, dass sich die API ändern kann – pinnen Sie also Ihre Version und rechnen Sie mit Änderungen.
Fazit
Shortcuts scheitern an vorhersehbaren Stellen: am Event-Target, an der Modifier-Taste, am Sequenz-Buffer und am Listener-Lebenszyklus. Beginnen Sie mit dem isTyping-Guard und dem metaKey || ctrlKey-Abgleich in dem Handler, den Sie bereits haben, und verschieben Sie anschließend Ihre Bindings in ein einziges Array, damit der Matcher und der ?-Hilfedialog aus derselben Quelle lesen.
FAQs
Was ist der Unterschied zwischen event.key und event.code bei Tastaturkürzeln?
event.key liefert das Zeichen, das eine Taste erzeugt, sobald Tastaturlayout und gehaltene Modifier berücksichtigt sind, während event.code die physische Tastenposition benennt und unabhängig vom Layout gleich bleibt. Gleichen Sie Shortcuts über event.key ab, damit ein 'k'-Binding auf jeder Tastatur den auf der Taste aufgedruckten Buchstaben bedeutet. Verwenden Sie event.code für positionsbasierte Eingaben wie WASD in Spielen. TanStack Hotkeys greift nur bei Buchstaben- und Zifferntasten auf event.code zurück, und auch nur dann, wenn event.key stattdessen ein Sonderzeichen zurückgibt, wie es unter macOS bei Option plus Buchstabe der Fall ist.
Sollte ich keydown, keyup oder keypress für Tastaturkürzel verwenden?
Verwenden Sie keydown. MDN kennzeichnet keypress als veraltet, und es wird nur für Tasten ausgelöst, die ein Zeichen erzeugen, sodass es Escape, Pfeiltasten oder einen allein gedrückten Modifier nie meldet. keydown wird für jede Taste ausgelöst, stellt event.key und die Modifier-Booleans bereit und ist das Event, bei dem preventDefault die browsereigene Aktion unterbindet. keyup trifft ein, nachdem der Browser bereits auf das keydown reagiert hat, und kann daher weder ein natives Shortcut noch ein eingefügtes Zeichen unterdrücken.
Werden Tastaturkürzel ausgelöst, während ein Nutzer mit einem IME wie japanischer oder chinesischer Eingabe tippt?
Ja. Ein keydown-Listener auf Dokumentebene empfängt Tastenanschläge auch, während ein IME komponiert – kehren Sie daher frühzeitig zurück, wenn event.isComposing true ist. Das Flag bleibt für jedes Tastatur-Event zwischen dem Öffnen und dem Schließen einer Kompositionssitzung des IME true, also genau in dem Zeitfenster, in dem Ihre Shortcuts sich zurückhalten sollten. Der isTyping-Guard fängt die meisten Fälle ab, weil die Komposition in einem editierbaren Element stattfindet, aber isComposing fügt eine zweite Prüfung für eigene Texteingabeflächen hinzu.