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:
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> )
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.
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> )
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:
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> ) }
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:
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> )
When nested scopes share the same hotkey, the innermost focused element takes precedence.