Skip to content

useKeyboardShortcut

What this solves

Keyboard shortcuts are a core productivity feature, but implementations often become inconsistent:

  • Different components bind keys differently
  • Inputs accidentally trigger global shortcuts
  • Handlers become stale due to closures
  • Shortcuts are hard to enable/disable based on UI state

useKeyboardShortcut provides a minimal, consistent way to register document-level shortcuts with options for enabling, preventing default, and ignoring inputs.


When to use it

Use useKeyboardShortcut for:

  • Global app shortcuts (e.g., open command palette)
  • Modal/dialog keyboard behaviors (Escape to close)
  • Page-level workflows (toggle help, open search)

When NOT to use it

Do not use useKeyboardShortcut when:

  • You only need key handling inside a single input (use the input’s onKeyDown)
  • You need complex key mapping/remapping or user-configurable bindings (this hook is intentionally simple)
  • You need to listen on a specific element instead of the document

Basic usage

tsx
import { useKeyboardShortcut } from '@/shared/hooks'

export function Example() {
  useKeyboardShortcut(
    { key: 'k', ctrl: true },
    () => {
      // open command palette
    },
    { preventDefault: true }
  )

  return <div />
}

Common patterns

Pattern 1 — Scoped shortcuts via enabled

Conditionally enable shortcuts based on UI state (modal open, drawer focused, etc.).

tsx
import { useKeyboardShortcut } from '@/shared/hooks'

export function Modal({ open, onClose }: { open: boolean; onClose: () => void }) {
  useKeyboardShortcut(
    { key: 'Escape' },
    () => onClose(),
    { enabled: open }
  )

  return open ? <div role="dialog" /> : null
}

Why this is recommended:

  • Avoids global shortcuts firing when the UI state doesn’t make sense
  • Keeps shortcut logic colocated with the owning component

Pattern 2 — Allow shortcuts even while typing (carefully)

By default, shortcuts ignore inputs/contenteditable. If you explicitly want a shortcut while focus is in an input, set ignoreInputs: false.

tsx
import { useKeyboardShortcut } from '@/shared/hooks'

export function SearchPage({ onOpen }: { onOpen: () => void }) {
  useKeyboardShortcut(
    { key: '/' },
    () => onOpen(),
    { ignoreInputs: false }
  )

  return <div />
}

Things to avoid

  • Registering shortcuts for the same key combo in multiple mounted components (the first matching handler wins in useKeyboardShortcuts, but useKeyboardShortcut will register multiple listeners)
  • Relying on shortcuts for text inputs by default (keep ignoreInputs: true unless you have a strong reason)
  • Assuming capture-phase behavior: the listener is attached to document in the default phase, so code that stops propagation may prevent the shortcut from firing


API reference (concise)

ts
export interface KeyboardShortcut {
  key: string
  ctrl?: boolean
  cmd?: boolean
  shift?: boolean
  alt?: boolean
}

export interface KeyboardShortcutOptions {
  enabled?: boolean
  preventDefault?: boolean
  ignoreInputs?: boolean
}

export function useKeyboardShortcut(
  shortcut: KeyboardShortcut,
  handler: (event: KeyboardEvent) => void,
  options?: KeyboardShortcutOptions
): void

export function useKeyboardShortcuts(
  shortcuts: Array<{
    shortcut: KeyboardShortcut
    handler: (event: KeyboardEvent) => void
    options?: KeyboardShortcutOptions
  }>,
  globalOptions?: KeyboardShortcutOptions
): void

Final notes

  • ctrl: true matches both Ctrl (Windows/Linux) and Cmd (macOS) because the implementation treats them as a single “Ctrl/Cmd” modifier.
  • Prefer discoverability: if a shortcut is important, also provide a visible UI affordance (button, menu item, tooltip) showing the key combo.