Glowing mechanical keyboard with TypeScript autocomplete annotations floating above its keys

TanStack Hotkeys: The Keyboard Shortcut Library That Types Back

The Gray Cat
The Gray Cat

Keyboard shortcuts are the silent contract between a developer and a power user. When they work well, nobody notices. When they do not exist, or when they conflict with browser defaults, or when they silently fail inside a text input, users notice immediately. Building a robust shortcut system from scratch means solving cross-platform modifier keys, handling input element edge cases, managing conflicts, formatting display labels, and a dozen other problems that sound trivial until you are knee-deep in KeyboardEvent quirks.

TanStack Hotkeys is the TanStack team's answer to this problem. From the creators of TanStack Query, Table, and Router, this library brings the same TypeScript-first, framework-agnostic philosophy to keyboard interactions. Its headline feature is a deeply typed hotkey string system that catches typos at compile time and provides IDE autocomplete for every valid modifier-plus-key combination. But beneath that headline lives a surprisingly complete toolkit: Vim-style multi-key sequences, a hotkey recorder for settings UIs, platform-aware display formatting, real-time key state tracking, and DevTools integration.

What Sets It Apart

Most keyboard shortcut libraries treat hotkey strings as plain strings. You type "ctrl+s" and hope you got the casing right. TanStack Hotkeys takes a radically different approach. The Hotkey type is a massive union of every valid combination, which means your editor will autocomplete "Mod+Shift+S" for you and flag "Mod+Ctrl+S" as invalid (since Mod already resolves to Ctrl on Windows, that combination would be redundant).

Beyond type safety, the library ships with smart defaults that handle the most common pain points out of the box. The ignoreInputs option defaults to "smart", which means modifier-based shortcuts like Mod+S still fire when you are typing inside a text field (because you probably do want to save), while plain letter keys stay quiet so they do not hijack your typing. Conflict detection warns you when two components try to register the same shortcut, and the cross-platform Mod modifier maps to Meta on macOS and Control everywhere else.

Getting Started

Install the React adapter. It re-exports everything from the core package, so you do not need a separate install.

npm install @tanstack/react-hotkeys

or

yarn add @tanstack/react-hotkeys

Your First Shortcut

Registering a Hotkey

The useHotkey hook is the primary API. Pass a hotkey string and a callback, and the library handles the rest.

import { useHotkey } from '@tanstack/react-hotkeys'

function SaveButton() {
  useHotkey('Mod+S', (event, { hotkey, parsedHotkey }) => {
    console.log(`${hotkey} triggered a save`)
    saveDocument()
  })

  return <button>Save</button>
}

That single line gives you cross-platform support (Cmd+S on Mac, Ctrl+S on Windows), prevents the browser's default save dialog, stops event propagation, and intelligently fires even when the user is focused on an input field. All of these behaviors come from the smart defaults.

Conditional and Scoped Shortcuts

Shortcuts can be toggled on and off with the enabled option, and scoped to specific DOM elements using a ref.

import { useRef, useState } from 'react'
import { useHotkey } from '@tanstack/react-hotkeys'

function Modal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
  const modalRef = useRef<HTMLDivElement>(null)

  useHotkey('Escape', () => onClose(), {
    enabled: isOpen,
    target: modalRef,
  })

  if (!isOpen) return null

  return (
    <div ref={modalRef} tabIndex={-1}>
      <p>Press Escape to close this modal.</p>
    </div>
  )
}

The target option ensures the Escape handler only fires when the modal element (or its children) has focus. This is particularly useful when multiple panels or dialogs coexist on the same page.

Displaying Shortcuts to Users

One of the most practical utilities in the library is formatForDisplay, which converts a hotkey string into a platform-appropriate label.

import { formatForDisplay, formatWithLabels } from '@tanstack/react-hotkeys'

// On macOS:
formatForDisplay('Mod+Shift+S')  // "⇧⌘S"
formatWithLabels('Mod+S')         // "Cmd+S"

// On Windows/Linux:
formatForDisplay('Mod+Shift+S')  // "Ctrl+Shift+S"
formatWithLabels('Mod+S')         // "Ctrl+S"

No more conditional rendering based on navigator.platform. The library detects the platform once and formats accordingly, using proper Unicode symbols on macOS and readable labels everywhere else.

Power User Territory

Vim-Style Sequences

For applications that need multi-key sequences (text editors, productivity tools, or anything inspired by Vim), the useHotkeySequence hook handles overlapping sequences with a configurable timeout.

import { useHotkeySequence } from '@tanstack/react-hotkeys'

function VimNavigator() {
  useHotkeySequence(['G', 'G'], () => {
    window.scrollTo({ top: 0, behavior: 'smooth' })
  })

  useHotkeySequence(['G', 'Shift+G'], () => {
    window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })
  })

  useHotkeySequence(['D', 'I', 'W'], () => {
    deleteInnerWord()
  })

  return <div>Vim-style navigation enabled</div>
}

Each sequence has a default timeout of 1000ms between keystrokes. If the user pauses too long, the sequence resets. You can customize this globally through the HotkeysProvider or per-sequence.

Recording Custom Shortcuts

Building a keyboard shortcut customization UI is one of those features that sounds simple and turns into a multi-day project. The useHotkeyRecorder hook handles the hard parts: listening for key combinations, ignoring incomplete modifier presses, and providing a clean API for your settings panel.

import { useState } from 'react'
import { useHotkey, useHotkeyRecorder, formatForDisplay } from '@tanstack/react-hotkeys'
import type { Hotkey } from '@tanstack/react-hotkeys'

function ShortcutSettings() {
  const [saveShortcut, setSaveShortcut] = useState<Hotkey>('Mod+S')

  const recorder = useHotkeyRecorder({
    onRecord: (hotkey) => setSaveShortcut(hotkey),
    onCancel: () => console.log('Recording cancelled'),
  })

  useHotkey(saveShortcut, () => handleSave())

  return (
    <div>
      <label>Save shortcut:</label>
      <button onClick={recorder.startRecording}>
        {recorder.isRecording
          ? recorder.recordedHotkey ?? 'Press keys...'
          : formatForDisplay(saveShortcut)}
      </button>
      {recorder.isRecording && (
        <button onClick={recorder.cancelRecording}>Cancel</button>
      )}
    </div>
  )
}

The recorder exposes isRecording, recordedHotkey, startRecording, stopRecording, and cancelRecording. Combined with formatForDisplay, you get a complete shortcut customization UI in about 20 lines.

Tracking Held Keys

Some interfaces change behavior based on which keys are currently held. Think of holding Shift for multi-select, or Alt for alternate drag behavior. The useKeyHold and useHeldKeys hooks provide reactive state for exactly this.

import { useKeyHold, useHeldKeys } from '@tanstack/react-hotkeys'

function SelectableList({ items }: { items: string[] }) {
  const isShiftHeld = useKeyHold('Shift')
  const heldKeys = useHeldKeys()

  return (
    <ul>
      {items.map((item) => (
        <li
          key={item}
          onClick={() => {
            if (isShiftHeld) {
              extendSelection(item)
            } else {
              selectSingle(item)
            }
          }}
        >
          {item}
        </li>
      ))}
      {heldKeys.length > 0 && (
        <div>Currently held: {heldKeys.join(' + ')}</div>
      )}
    </ul>
  )
}

Wiring It All Together

Provider and DevTools

The HotkeysProvider lets you set global defaults for all hooks in your application. Pair it with the DevTools plugin to get a visual panel showing every registered shortcut, their current state, and the ability to trigger them programmatically during development.

import { HotkeysProvider } from '@tanstack/react-hotkeys'
import { TanStackDevtools } from '@tanstack/react-devtools'
import { hotkeysDevtoolsPlugin } from '@tanstack/react-hotkeys-devtools'

function Root() {
  return (
    <HotkeysProvider
      defaultOptions={{
        hotkey: {
          preventDefault: true,
          conflictBehavior: 'warn',
        },
        hotkeySequence: { timeout: 1500 },
      }}
    >
      <App />
      <TanStackDevtools plugins={[hotkeysDevtoolsPlugin()]} />
    </HotkeysProvider>
  )
}

The DevTools panel shows all registered hotkeys with their options, lets you see which keys are currently held in real-time, and allows you to programmatically trigger callbacks for testing. It is automatically excluded from production builds.

Validation for Dynamic Shortcuts

When accepting user-defined shortcuts (from a settings panel or configuration file), the validateHotkey function checks whether a string represents a valid combination and returns platform-specific warnings.

import { validateHotkey } from '@tanstack/react-hotkeys'

const result = validateHotkey('Alt+A')
// {
//   valid: true,
//   warnings: ['Alt+letter combinations may not work on macOS...'],
//   errors: []
// }

const invalid = validateHotkey('Banana+S')
// {
//   valid: false,
//   warnings: [],
//   errors: ['Invalid modifier: Banana']
// }

This is especially useful for building robust shortcut customization interfaces where you want to warn users about combinations that might not work on their platform.

A Note on Maturity

TanStack Hotkeys is roughly one month old as of this writing, currently at version 0.2.0 with alpha-level stability. The API may still shift, and there are known gaps: some punctuation keys are not yet supported, sequences do not respect the ignoreInputs setting, and numpad keys are missing. Framework adapters exist for React and Solid, with Vue, Svelte, and Angular listed as needing contributors.

That said, the TanStack team has a proven track record of shipping and maintaining high-quality libraries. TanStack Query alone serves over 17 million weekly downloads. The architecture is solid, the TypeScript types are thorough, and the feature set is already more comprehensive than libraries that have been around for years.

Conclusion

@tanstack/react-hotkeys is not just another keyboard shortcut hook. It is a complete keyboard interaction toolkit that addresses the full lifecycle of shortcut management: defining them with type safety, registering them with smart defaults, formatting them for display, recording custom ones from users, tracking key state in real-time, and debugging everything through DevTools. For teams already invested in the TanStack ecosystem, it slots in naturally alongside Query, Router, and Table. For everyone else, the TypeScript autocomplete alone might be worth the adoption. Just keep an eye on the version number and be ready for a few API changes as the library matures toward a stable release.