Skip to main content
Version: 5.0

Scoping hotkeys to components

Global hotkeys and scoped components

In the previous section we used two keystrokes in two different examples (ctrl+shift+a+c and shift+c). If you press one of those keystrokes, both examples trigger. Why?

By default, hotkeys are attached globally — there is no built-in scoping mechanism that limits a hotkey to only trigger when its component is focused. Here are two components that both listen for the c key:

Live Editor
function UnscopedHotkey() {
  const [count, setCount] = useState(0)
  useHotkeys('c', () => setCount(prevCount => prevCount + 1))

  return (
    <p>The count is {count}.</p>
  )
}

function SecondUnscopedHotkey() {
  const [count, setCount] = useState(0)
  useHotkeys('c', () => setCount(prevCount => prevCount + 1))

  return (
    <p>The count is {count}.</p>
  )
}

render(
  <div>
    <UnscopedHotkey/>
    <SecondUnscopedHotkey/>
  </div>
)
Result
Loading...

Every time you press the c key, both counters increment. To isolate a hotkey to a specific component, useHotkeys returns a React ref callback that you attach to the element that should have focus before the hotkey fires.

Live Editor
function ScopedHotkey() {
  const [count, setCount] = useState(0)
  const ref = useHotkeys('shift+a', () => setCount(prevCount => prevCount + 1))

  return (
    <>
      <p>The count is {count}. Click anywhere except the button to disable the hotkey.</p>
      <button ref={ref}>
        Click me to enable the hotkey
      </button>
    </>
  )
}

function SecondScopedHotkey() {
  const [count, setCount] = useState(0)
  const ref = useHotkeys('shift+a', () => setCount(prevCount => prevCount + 1))

  return (
    <>
      <p>The count is {count}. Click anywhere except the button to disable the hotkey.</p>
      <button ref={ref}>
        Click me to enable the hotkey
      </button>
    </>
  )
}

render(
  <div>
    <ScopedHotkey/>
    <hr/>
    <SecondScopedHotkey/>
  </div>
)
Result
Loading...

Now each hotkey only fires when its assigned button has focus. You can use this technique for any hotkey, not just duplicates.


Scoping with non-focusable elements

Relying on a button for focus is rarely useful in real applications. More often, you want a container — like a modal or a card — to receive focus and enable its hotkeys. Elements like <div>, <section>, and <span> cannot receive focus by default. To make them focusable, add a tabIndex attribute:

Live Editor
function ScopedHotkey() {
  const [count, setCount] = useState(0)
  const ref = useHotkeys('shift+a', () => setCount(prevCount => prevCount + 1))

  return (
    <div ref={ref} tabIndex={-1} style={{border: '2px solid #9e768f', padding: '12px'}}>
      <p>The count is {count}. Click inside this area to enable the hotkey.</p>
    </div>
  )
}
Result
Loading...
Use tabIndex={-1}

Setting tabIndex to -1 makes an element focusable via JavaScript or mouse click, but keeps it out of the keyboard tab order. This prevents accidental disruption of the page's natural tab flow. For more details, see the MDN page on tabindex.


Nesting scoped hotkeys

Scoped hotkeys also work with nested components:

Live Editor
function NestedScopedHotkey() {
  const [count, setCount] = useState(0)
  const ref = useHotkeys('shift+a', () => setCount(prevCount => prevCount + 1))

  return (
    <div ref={ref} tabIndex={-1} style={{border: '2px solid deeppink', padding: '12px'}}>
      <p>The count is {count}. Click inside this area to enable the hotkey.</p>
    </div>
  )
}

function ScopedHotkey({children}) {
  const [count, setCount] = useState(0)
  const ref = useHotkeys('shift+a', () => setCount(prevCount => prevCount + 1))

  return (
    <div ref={ref} tabIndex={-1} style={{border: '2px solid #9e768f', padding: '12px'}}>
      <p>The count is {count}. Click inside this area to enable the hotkey.</p>
      {children}
    </div>
  )
}

render(
  <ScopedHotkey>
    <NestedScopedHotkey/>
  </ScopedHotkey>
)
Result
Loading...

When nested scopes share the same hotkey, the innermost focused element takes precedence.