Skip to content

Keyboard Workflows

Problem

Keyboard shortcuts are easy to add but easy to break:

  • Shortcuts fire while typing in inputs
  • Modals register Escape handlers inconsistently
  • Multiple components bind the same shortcut
  • Handlers become stale due to closure issues

Solution

Use useKeyboardShortcut (and useKeyboardShortcuts when needed) and model scope explicitly:

  • Global shortcuts: registered at the app shell level
  • Scoped shortcuts: register in the owning component and gate with enabled
  • Default to ignoreInputs: true to avoid interfering with typing

Implementation

Global shortcut that toggles UI

tsx
import React from 'react'
import { useKeyboardShortcut, useToggle } from '@/shared/hooks'

export function ShortcutsHelp() {
  const help = useToggle(false)

  useKeyboardShortcut(
    { key: '?' },
    () => help.toggle(),
    { ignoreInputs: true }
  )

  return (
    <>
      <button type="button" onClick={help.toggle}>Toggle help</button>
      {help.value && <div role="dialog">Help content</div>}
    </>
  )
}

Scoped shortcut enabled only while a modal is open

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" aria-modal="true" /> : null
}

Variations

  • Allow shortcuts while typing
    • Set ignoreInputs: false for shortcuts like / to focus search.
    • Use sparingly; document the behavior.
  • Multiple shortcuts in one place
    • Use useKeyboardShortcuts([...]) if you want a single listener that routes multiple key combos.

Accessibility considerations

  • Provide visible affordances for critical actions (buttons/menus) in addition to shortcuts.
  • Avoid overriding browser/OS shortcuts unless absolutely necessary.
  • Don’t assume capture-phase behavior: the listener is attached to document in the default phase; code that stops propagation can prevent a shortcut from firing.