TanStack
Guides

Sensible Defaults and Keyboard Layouts

Register Mod+S with no options and it behaves the way users expect: Command+S on macOS, Control+S elsewhere, the browser's Save dialog stays closed, it still works while the user is typing in a text field, and it works on any keyboard layout. TanStack Hotkeys chooses its defaults so the common case needs no configuration.

This guide describes each default, why it was chosen, and how to change it.

At a glance

BehaviorDefaultChange it with
Mod modifierCommand on macOS, Control on Windows and Linuxplatform
Key identityThe character the key types (event.key), with a fallback to the key's position[KeyS] physical bindings
ModifiersMust match exactlyList every modifier in the binding
Typing in inputsSingle keys and Shift/Alt chords are ignored; Control/Meta chords and Escape fireignoreInputs
Browser behaviorPrevented, and the event stops propagatingpreventDefault, stopPropagation
When a chord firesOn keydown, and again on each automatic repeat while heldeventType, requireReset
Where listeners attachdocumenttarget
When hotkeys runAfter the keyboard handlers of widgets inside the target (bubble phase)capture
Time between sequence steps1 secondtimeout
Duplicate registrationsAllowed, with a console warningconflictBehavior
RecordingRecords key positions; Escape cancelsrecordBy, recorder options
DisplaySymbols on macOS (⇧ ⌘ S), text elsewhere (Ctrl+Shift+S)formatForDisplay options

Mod follows the platform

Mod is the platform's primary shortcut modifier: Command (Meta) on macOS and Control on Windows and Linux. Write Mod+S once and each user gets the save shortcut their operating system has trained them to use. The platform is detected from navigator. During server rendering, where there is no navigator, it is linux.

Hotkeys are stored in a canonical form, so equivalent spellings are the same binding. On Windows and Linux, Control+S and ctrl+s are stored as Mod+S. On macOS, Control+S remains Control+S, because Control is not the primary modifier there. This is why two spellings conflict on one platform and not on another.

Pass platform: 'mac' | 'windows' | 'linux' to override detection, for example in tests or to preview another platform's shortcuts.

Keys match the character the user types

A logical binding such as Mod+S or ? matches event.key, the character the key produces on the active keyboard layout. A user on Dvorak, Colemak, or AZERTY presses the key labeled S to trigger Mod+S, wherever that key sits on their keyboard. Matching the key's position (event.code) instead would make Mod+Z fire on the AZERTY key labeled W, and on Dvorak would turn the key labeled X into a different shortcut.

event.key is not always useful, so the matcher falls back to the key's conventional position when the character cannot identify the shortcut:

  • Non-Latin layouts. On a Russian layout, the S position types ы. Mod+S still matches there, so shortcuts work without switching layouts.
  • macOS Option. Option changes the character, so Option+S types ß. Alt+S still matches.
  • Dead keys and shifted characters. A dead key reports Dead, and AZERTY's digit row types é for 2. The key's position resolves the shortcut.

ASCII letters never fall back. If a key types x, it matches X and nothing else, so a positional fallback can never hijack a Dvorak or Colemak user's shortcut. When one key press could match several registrations on the same target, an exact match takes priority over a fallback.

Letters match without regard to case: S and s are the same binding.

Match a key's position instead

When a shortcut should follow a position rather than a character, such as W/A/S/D movement in a game, use a physical binding. Physical bindings match event.code exactly and ignore the layout:

BindingMatches
Mod+S or { key: 'S', mod: true }The character S (event.key), with the fallbacks above
Mod+[KeyS] or { code: 'KeyS', mod: true }The key in the S position on a US layout (event.code)
EnterEither Enter key, including the numpad's
[Enter] or [NumpadEnter]Only that Enter key

On a layout where the KeyQ position types a, A follows the character and [KeyQ] follows the position.

  • In strings, every physical code goes in brackets, including codes that share a name with a logical key, such as [Enter] and [F13]. Codes are type-checked and autocomplete in your editor.
  • In objects, use the code field for a physical binding and key for a logical one. A binding has one or the other, never both, and key never takes brackets.
  • In a chord's callback, context.parsedChord.code is set only for physical bindings. The same goes for each step in a sequence's context.parsedSequence. Check code !== undefined before reading it.
  • formatForDisplay labels physical bindings without brackets, so Mod+[KeyS] displays as ⌘ S on macOS. The stored binding keeps its brackets.

The hotkey recorder records positions by default, so recorded hotkeys are physical bindings.

Modifiers must match exactly

Mod+S does not fire for Mod+Shift+S, and K does not fire for Shift+K. Exact matching lets Mod+Z (undo) and Mod+Shift+Z (redo) coexist without one swallowing the other.

The exception is Shift that is needed to type a symbol. On a US layout, ? is typed with Shift+/, so ? matches without listing Shift. This applies to printable symbols, not letters, so K and Shift+K remain different bindings.

Two kinds of key events never match ordinary shortcuts:

  • AltGr. On Windows and Linux, AltGr types characters such as @ and € on many layouts and reports itself as Control+Alt. Those key presses do not trigger Control+Alt shortcuts, so typing an email address does not run a command.
  • IME composition. While the user composes text with an input method editor, such as for Japanese, Chinese, or Korean, character and physical bindings do not fire. Named keys such as Escape still do.

Typing in inputs does not trigger shortcuts

A hotkey bound to K should not fire while the user types "okay" into a text field. By default, ignoreInputs is 'auto', which chooses from the hotkey itself:

HotkeyDefaultReason
K, Shift+K, Alt+K, ?Ignored in inputsThese keys type text
Mod+S, Control+K, Meta+EnterFire in inputsControl and Meta chords don't type text, and users expect save and similar commands to work from a field
EscapeFires in inputsUsers expect Escape to close a dialog even when a field inside it has focus

For a sequence, the first step decides, so ['G', 'G'] is ignored in inputs.

Text inputs, textareas, selects, and contenteditable elements count as inputs, including inside shadow DOM. Button-type inputs (type="button", "submit", and "reset") do not, so shortcuts still work when the user has tabbed to a form button. A hotkey whose target is the input itself always receives that input's events.

Set ignoreInputs: true or false to choose explicitly. For example, set ignoreInputs: false on an Enter hotkey that submits a form, so it works while focus is in the form's text fields.

Focused controls keep their own keys

Buttons, links, and ARIA widgets also use keys for themselves. A hotkey bound to Enter should not stop a focused button from clicking, and a hotkey bound to ArrowDown should not also run while the user moves through an open menu. When ignoreInputs is on, a hotkey skips these keys while such a control has focus:

Focused controlKeys it keeps
<button>, button-type inputs, <summary>, role="button"Enter, Space
Links with an href, role="link"Enter
role="checkbox", "switch", "radio"Space
role="tab", "option", "treeitem", "menuitem" (and its checkbox and radio forms)Enter, Space
role="menu", "menubar", "tree"Arrow keys, Home, End, Enter, Space, and typed characters
role="listbox"Up and Down (Left and Right with aria-orientation="horizontal"), Home, End, Enter, Space, and typed characters
role="combobox" without a text fieldUp, Down, Home, End, Enter, Space, and typed characters
role="tablist"Left and Right (Up and Down with aria-orientation="vertical"), Home, End, Enter, Space
role="toolbar"Left and Right (Up and Down with aria-orientation="vertical"), Home, End
role="grid", "treegrid"Arrow keys, Home, End, Page Up, Page Down, Enter, Space
role="radiogroup"Arrow keys, Space
role="slider"Arrow keys, Home, End, Page Up, Page Down
role="spinbutton"Up, Down, Home, End, Page Up, Page Down

The keys follow the WAI-ARIA Authoring Practices keyboard patterns. A control counts while it, or an element inside it, has focus, so arrow keys in a menu item belong to the menu. Typed characters are the menu, listbox, or tree's typeahead search.

Control, Meta, and Alt shortcuts are never skipped. Enter and Space are skipped only without modifiers. Navigation keys and typed characters are also skipped with Shift, which widgets use to extend a selection.

A control counts only when it is inside the hotkey's target. A hotkey targeted at a dialog skips Enter while the dialog's Cancel button has focus, but a hotkey targeted at a listbox receives the listbox's arrow keys, so the listbox can implement its own navigation with hotkeys. Set ignoreInputs: false to handle every key regardless of focus.

The recorder only skips text fields, so it can record Enter or Space while the button that started recording still has focus.

The browser's action is prevented

preventDefault and stopPropagation are true by default. Most shortcuts exist to replace a browser action: binding Mod+S to save a document should not also open the browser's Save Page dialog, and binding Mod+K to a command palette should not also focus the address bar. Stopping propagation keeps one key press from running both your shortcut and another listener higher in the page.

Both apply only when a hotkey matches; every other key press is left alone. For a sequence, every step that advances it calls preventDefault, so the browser doesn't act on Mod+K in ['Mod+K', 'Mod+C']. Propagation stops only on the last step, so earlier steps still reach other listeners. Set either option to false when you want to observe a shortcut without taking it over.

Some browser and operating system shortcuts never reach the page and cannot be overridden. Browsers keep Mod+T, Mod+N, and Mod+W for opening and closing tabs and windows, and the operating system keeps shortcuts such as Alt+Tab and Command+Tab. Pick other keys for your app.

Single-key shortcuts need an off switch

Ignoring inputs keeps a K hotkey from firing while the user types in a field, but it does not cover every case. Speech input users dictate words that arrive as key presses, and a stray key press can run a command. WCAG 2.1.4 requires that a shortcut made of a single letter, number, punctuation, or symbol key meets at least one of these conditions:

  • Users can turn it off. Pass enabled from a user setting.
  • Users can remap it to include a modifier such as Control. App hotkeys let users rebind or unbind any hotkey you declare.
  • It only works while a component has focus. Set target to that component's element.

Shortcuts with Mod, Control, or Alt, and keys that don't type characters, such as Escape and the function keys, are not affected. Shift doesn't count as a modifier here, because Shift+K still types a letter.

Chords fire on keydown and repeat while held

Chords fire on keydown, when the user presses the key, rather than on release. Holding a key fires the chord again on each automatic repeat, as holding Control+Z in a text editor repeats undo.

For a toggle, such as opening and closing a panel, repeating is usually wrong: holding the key would make the panel flicker. Set requireReset: true to fire once per press. Releasing the key or any of its modifiers allows the chord to fire again.

Set eventType: 'keyup' to fire on release instead, for example to end a mode that started when a key went down.

Listeners attach to the document

Hotkeys listen on document by default, so they work wherever focus is in the page. The library attaches one keydown and one keyup listener per target, plus another pair if any hotkey on that target uses capture, however many hotkeys are registered.

To limit a hotkey to part of the page, set target to an element. The element receives keyboard events only while it, or something inside it, has focus, so make sure it is focusable, for example with tabIndex.

A hotkey registered from the page can also target the document, window, or an element of a same-origin iframe or popup window. The hotkey then listens in that frame, and typing in the frame's inputs is ignored the same way as in the page.

No listeners are attached during server rendering.

Hotkeys run after the page's own handlers

Hotkeys listen during the bubble phase, so a focused widget's own keyboard handler runs first. If a tab strip, editor, or menu handles a key and stops propagation, your hotkey never sees it. Usually that is what users expect: the widget they are working in should win.

Set capture: true when an app-level shortcut has to win anyway. It works with both keydown and keyup hotkeys. For example, a tab strip that ignores modifiers might treat Mod+Backspace as Backspace and close a tab before a document-level "close view" hotkey runs. With capture: true, the hotkey runs first, and the default stopPropagation: true keeps the key from the tab strip. Set stopPropagation: false if the widget should still receive it.

Capture doesn't change which keys a hotkey skips. Typing in inputs and keys that focused controls keep for themselves are still skipped while ignoreInputs is on.

Sequences allow 1 second between steps

A sequence such as ['G', 'G'] completes when each step follows the previous one within 1 second. One second is long enough for deliberate typing and short enough that two unrelated key presses rarely combine. Change it with timeout.

Sequences are forgiving in a few other ways:

  • Pressing a modifier alone does not interrupt a sequence, so steps can include modifiers: ['Shift+R', 'Shift+T'].
  • Automatic repeats are not steps. Holding G does not complete ['G', 'G'].
  • A wrong key restarts the sequence. If the wrong key is the sequence's first step, it begins a new attempt, so G X G G still completes ['G', 'G'].
  • Progress clears when the timeout passes, without waiting for another key press, so a UI that shows the pressed steps clears on time.

Duplicate registrations warn

Registering a hotkey that is already registered on the same target logs a console warning, and both callbacks run. A warning surfaces accidental duplicates during development without breaking an app that intentionally shares a shortcut. Duplicates are compared in canonical form, so Mod+S and Meta+S are duplicates on macOS.

Set conflictBehavior to 'replace' to replace the existing registration, 'error' to throw, or 'allow' to silence the warning.

Recording captures what the user pressed

The hotkey recorder records physical key positions by default (recordBy: 'code'). A modifier can change which character a key types (Option+S types ß, and Shift+1 types !), but its position does not change. Recording the position saves the key the user pressed, regardless of what it typed. Set recordBy: 'key' to record characters instead.

While recording:

  • Escape cancels.
  • Backspace or Delete removes the last step, or clears an empty recording.
  • Enter commits a sequence recording, made with maxSteps: Infinity.
  • Registered hotkeys are paused, so pressing Mod+S to record it does not also save.
  • Typing in inputs passes through, so other fields on the page keep working. Escape still cancels.
  • AltGr is rejected for position recordings, because it types characters rather than shortcuts.

Held keys reset when the window loses focus

Key state tracking marks a key as held on keydown and releases it on keyup. If the window loses focus while a key is down, such as when the user switches apps with Command+Tab, the browser never delivers the keyup. Held keys are therefore cleared when the window loses focus.

On macOS, the system can also swallow the keyup of a key pressed with Command. Releasing a modifier clears any other keys still marked as held, so no key is stuck.

Some system shortcuts swallow a modifier's own keyup without taking focus from the page, such as Command+Shift+4 for a screenshot on macOS, or the DevTools inspect shortcut. Every keyboard and mouse event reports which modifiers are down, so the next key press, mouse movement, or click releases any modifier it reports as up. Only real user input counts: events that page code dispatches report no modifiers, so the tracker ignores them for this. Until such an event arrives, the modifier stays held, because nothing distinguishes a missed release from a long hold.

Display matches the platform

formatForDisplay uses each platform's conventions. On macOS, Mod+Shift+S displays as ⇧ ⌘ S. On Windows and Linux, it displays as Ctrl+Shift+S. Sequence steps are separated by a space: G G.

Pass useSymbols: false for text labels on macOS, or keyLabels to rename individual keys.

Changing the defaults

Every option above can be set on a single registration. To change a default for many hotkeys at once, set it in your framework adapter's shared defaults.

Pass defaultOptions to HotkeysProvider. See Hotkey options in Register Hotkeys.