Docs
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
API Reference
Hotkeys API Reference
Hotkey Sequence API Reference
Key Hold API Reference
Held Keys API Reference
Hotkey Recorder API Reference
Hotkey Sequence Recorder API Reference
Format for Display API Reference

React Example: UseHotkeyRecorder

tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import {
  HotkeysProvider,
  formatForDisplay,
  useHeldKeys,
  useHotkey,
  useHotkeyRecorder,
} from '@tanstack/react-hotkeys'
import { hotkeysDevtoolsPlugin } from '@tanstack/react-hotkeys-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
import './index.css'
import type { Hotkey } from '@tanstack/react-hotkeys'

interface ShortcutActions {
  [key: string]: {
    name: string
    defaultHotkey: Hotkey
  }
}

const DEFAULT_SHORTCUT_ACTIONS: ShortcutActions = {
  save: {
    name: 'Save',
    defaultHotkey: 'Mod+K',
  },
  open: {
    name: 'Open',
    defaultHotkey: 'Mod+E',
  },
  new: {
    name: 'New',
    defaultHotkey: 'Mod+G',
  },
  close: {
    name: 'Close',
    defaultHotkey: 'Mod+Shift+K',
  },
  undo: {
    name: 'Undo',
    defaultHotkey: 'Mod+Shift+E',
  },
  redo: {
    name: 'Redo',
    defaultHotkey: 'Mod+Shift+G',
  },
}

function App() {
  // State for shortcuts
  const [shortcuts, setShortcuts] = React.useState<Record<string, Hotkey | ''>>(
    () => {
      const defaults: Record<string, Hotkey> = {}
      for (const [id, action] of Object.entries(DEFAULT_SHORTCUT_ACTIONS)) {
        defaults[id] = action.defaultHotkey
      }
      return defaults
    },
  )

  // Simple counters for each action
  const [saveCount, setSaveCount] = React.useState(0)
  const [openCount, setOpenCount] = React.useState(0)
  const [newCount, setNewCount] = React.useState(0)
  const [closeCount, setCloseCount] = React.useState(0)
  const [undoCount, setUndoCount] = React.useState(0)
  const [redoCount, setRedoCount] = React.useState(0)

  // Track which action is being recorded
  const [recordingActionId, setRecordingActionId] = React.useState<
    string | null
  >(null)

  // Use the library's useHotkeyRecorder hook
  const recorder = useHotkeyRecorder({
    onRecord: (hotkey: Hotkey) => {
      if (recordingActionId) {
        setShortcuts((prev) => ({
          ...prev,
          [recordingActionId]: hotkey,
        }))
        setRecordingActionId(null)
      }
    },
    onCancel: () => {
      setRecordingActionId(null)
    },
    onClear: () => {
      if (recordingActionId) {
        setShortcuts((prev) => ({
          ...prev,
          [recordingActionId]: '' as Hotkey | '',
        }))
        setRecordingActionId(null)
      }
    },
  })

  // Register shortcuts using useHotkey
  const isRecording = recorder.isRecording

  // Get actual hotkey values (use defaults if empty)
  const saveHotkey: Hotkey =
    shortcuts.save || DEFAULT_SHORTCUT_ACTIONS.save.defaultHotkey
  const openHotkey: Hotkey =
    shortcuts.open || DEFAULT_SHORTCUT_ACTIONS.open.defaultHotkey
  const newHotkey: Hotkey =
    shortcuts.new || DEFAULT_SHORTCUT_ACTIONS.new.defaultHotkey
  const closeHotkey: Hotkey =
    shortcuts.close || DEFAULT_SHORTCUT_ACTIONS.close.defaultHotkey
  const undoHotkey: Hotkey =
    shortcuts.undo || DEFAULT_SHORTCUT_ACTIONS.undo.defaultHotkey
  const redoHotkey: Hotkey =
    shortcuts.redo || DEFAULT_SHORTCUT_ACTIONS.redo.defaultHotkey

  // Register each shortcut - only enable if shortcut is actually set (not empty)
  useHotkey(
    saveHotkey,
    () => {
      console.log('Save triggered:', saveHotkey)
      setSaveCount((c) => c + 1)
    },
    {
      enabled: !isRecording && shortcuts.save !== '',
    },
  )

  useHotkey(
    openHotkey,
    () => {
      console.log('Open triggered:', openHotkey)
      setOpenCount((c) => c + 1)
    },
    {
      enabled: !isRecording && shortcuts.open !== '',
    },
  )

  useHotkey(
    newHotkey,
    () => {
      console.log('New triggered:', newHotkey)
      setNewCount((c) => c + 1)
    },
    {
      enabled: !isRecording && shortcuts.new !== '',
    },
  )

  useHotkey(
    closeHotkey,
    () => {
      console.log('Close triggered:', closeHotkey)
      setCloseCount((c) => c + 1)
    },
    {
      enabled: !isRecording && shortcuts.close !== '',
    },
  )

  useHotkey(
    undoHotkey,
    () => {
      console.log('Undo triggered:', undoHotkey)
      setUndoCount((c) => c + 1)
    },
    {
      enabled: !isRecording && shortcuts.undo !== '',
    },
  )

  useHotkey(
    redoHotkey,
    () => {
      console.log('Redo triggered:', redoHotkey)
      setRedoCount((c) => c + 1)
    },
    {
      enabled: !isRecording && shortcuts.redo !== '',
    },
  )

  const handleEdit = (actionId: string) => {
    setRecordingActionId(actionId)
    recorder.startRecording()
  }

  const handleCancel = () => {
    recorder.cancelRecording()
    setRecordingActionId(null)
  }

  return (
    <div className="app">
      <header>
        <h1>Keyboard Shortcuts Settings</h1>
        <p>
          Customize your keyboard shortcuts. Click "Edit" to record a new
          shortcut, or press Escape to cancel.
        </p>
      </header>

      <main>
        <section className="demo-section">
          <h2>Shortcuts</h2>
          <div className="shortcuts-list">
            {Object.entries(DEFAULT_SHORTCUT_ACTIONS).map(
              ([actionId, action]) => (
                <ShortcutListItem
                  key={actionId}
                  actionName={action.name}
                  hotkey={shortcuts[actionId] || ''}
                  isRecording={
                    recorder.isRecording && recordingActionId === actionId
                  }
                  onEdit={() => handleEdit(actionId)}
                  onCancel={handleCancel}
                />
              ),
            )}
          </div>
        </section>

        <section className="demo-section">
          <h2>Demo Actions</h2>
          <p>Try your shortcuts! Actions will trigger when you press them.</p>
          <div className="demo-stats">
            <div className="stat-item">
              <div className="stat-label">Save</div>
              <div className="stat-value">{saveCount}</div>
              <kbd>{formatForDisplay(shortcuts.save || 'Mod+K')}</kbd>
            </div>
            <div className="stat-item">
              <div className="stat-label">Open</div>
              <div className="stat-value">{openCount}</div>
              <kbd>{formatForDisplay(shortcuts.open || 'Mod+E')}</kbd>
            </div>
            <div className="stat-item">
              <div className="stat-label">New</div>
              <div className="stat-value">{newCount}</div>
              <kbd>{formatForDisplay(shortcuts.new || 'Mod+G')}</kbd>
            </div>
            <div className="stat-item">
              <div className="stat-label">Close</div>
              <div className="stat-value">{closeCount}</div>
              <kbd>{formatForDisplay(shortcuts.close || 'Mod+Shift+K')}</kbd>
            </div>
            <div className="stat-item">
              <div className="stat-label">Undo</div>
              <div className="stat-value">{undoCount}</div>
              <kbd>{formatForDisplay(shortcuts.undo || 'Mod+Shift+E')}</kbd>
            </div>
            <div className="stat-item">
              <div className="stat-label">Redo</div>
              <div className="stat-value">{redoCount}</div>
              <kbd>{formatForDisplay(shortcuts.redo || 'Mod+Shift+G')}</kbd>
            </div>
          </div>
        </section>

        {recorder.isRecording && (
          <div className="info-box recording-notice">
            <strong>Recording shortcut...</strong> Press any key combination or
            Escape to cancel. Press Backspace/Delete to clear the shortcut.
          </div>
        )}

        <section className="demo-section">
          <h2>Usage</h2>
          <pre className="code-block">{`import { useHotkey, formatForDisplay } from '@tanstack/react-hotkeys'

function App() {
  const [shortcuts, setShortcuts] = useState({
    save: 'Mod+K',
    open: 'Mod+E',
  })

  // Register shortcuts dynamically
  useHotkey(
    shortcuts.save,
    () => handleSave(),
    { enabled: !isRecording }
  )

  return (
    <div>
      <kbd>{formatForDisplay(shortcuts.save)}</kbd>
    </div>
  )
}`}</pre>
        </section>
      </main>
      <TanStackDevtools plugins={[hotkeysDevtoolsPlugin()]} />
    </div>
  )
}

interface ShortcutListItemProps {
  actionName: string
  hotkey: string
  isRecording: boolean
  onEdit: () => void
  onCancel: () => void
}

function ShortcutListItem({
  actionName,
  hotkey,
  isRecording,
  onEdit,
  onCancel,
}: ShortcutListItemProps) {
  const heldKeys = useHeldKeys()

  return (
    <div className={`shortcut-item ${isRecording ? 'recording' : ''}`}>
      <div className="shortcut-item-content">
        <div className="shortcut-action">{actionName}</div>
        <div className="shortcut-hotkey">
          {isRecording ? (
            <div className="recording-indicator">
              {heldKeys.length > 0 ? (
                <div className="held-hotkeys">
                  {heldKeys.map((key, index) => (
                    <React.Fragment key={key}>
                      {index > 0 && <span className="plus">+</span>}
                      <kbd>{key}</kbd>
                    </React.Fragment>
                  ))}
                </div>
              ) : (
                <span className="recording-text">
                  Press any key combination...
                </span>
              )}
            </div>
          ) : hotkey ? (
            <kbd>{formatForDisplay(hotkey as Hotkey)}</kbd>
          ) : (
            <span className="no-shortcut">No shortcut</span>
          )}
        </div>
      </div>
      <div className="shortcut-actions">
        {isRecording ? (
          <button onClick={onCancel} className="cancel-button">
            Cancel
          </button>
        ) : (
          <button onClick={onEdit} className="edit-button">
            Edit
          </button>
        )}
      </div>
    </div>
  )
}

ReactDOM.createRoot(document.getElementById('root')!).render(
  // optionally, provide default options to an optional HotkeysProvider
  <HotkeysProvider
  // defaultOptions={{
  //   hotkey: {
  //     preventDefault: true,
  //   },
  // }}
  >
    <App />
  </HotkeysProvider>,
)