Appearance
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, butuseKeyboardShortcutwill register multiple listeners) - Relying on shortcuts for text inputs by default (keep
ignoreInputs: trueunless you have a strong reason) - Assuming capture-phase behavior: the listener is attached to
documentin the default phase, so code that stops propagation may prevent the shortcut from firing
Related primitives
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
): voidFinal notes
ctrl: truematches 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.