TanStack Hotkeys supports multi-key sequences -- shortcuts where you press keys one after another rather than simultaneously. This is commonly used for Vim-style navigation, cheat codes, or multi-step commands.
Use the hook to register a key sequence:
import { useHotkeySequence } from '@tanstack/react-hotkeys'
function App() {
// Vim-style: press g then g to scroll to top
useHotkeySequence(['G', 'G'], () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
})
}import { useHotkeySequence } from '@tanstack/react-hotkeys'
function App() {
// Vim-style: press g then g to scroll to top
useHotkeySequence(['G', 'G'], () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
})
}The first argument is an array of strings representing each step in the sequence. The user must press them in order within the timeout window.
When you need several sequences—or a dynamic list whose length is not fixed at compile time—use instead of calling many times. One hook call keeps you within the rules of hooks while still registering every sequence.
import { useHotkeySequences } from '@tanstack/react-hotkeys'
useHotkeySequences([
{ sequence: ['G', 'G'], callback: () => scrollToTop() },
{ sequence: ['D', 'D'], callback: () => deleteLine(), options: { timeout: 500 } },
])import { useHotkeySequences } from '@tanstack/react-hotkeys'
useHotkeySequences([
{ sequence: ['G', 'G'], callback: () => scrollToTop() },
{ sequence: ['D', 'D'], callback: () => deleteLine(), options: { timeout: 500 } },
])Options merge in the same order as : defaults, then the second-argument , then each definition’s .
The third argument is an options object:
useHotkeySequence(['G', 'G'], callback, {
timeout: 1000, // Time allowed between keys (ms)
enabled: true, // Whether the sequence is active
})useHotkeySequence(['G', 'G'], callback, {
timeout: 1000, // Time allowed between keys (ms)
enabled: true, // Whether the sequence is active
})The maximum time (in milliseconds) allowed between consecutive key presses. If the user takes longer than this between any two keys, the sequence resets. Defaults to (1 second).
// Fast sequence - user must type quickly
useHotkeySequence(['D', 'D'], () => deleteLine(), { timeout: 500 })
// Slow sequence - user has more time between keys
useHotkeySequence(['Shift+Z', 'Shift+Z'], () => forceQuit(), { timeout: 2000 })// Fast sequence - user must type quickly
useHotkeySequence(['D', 'D'], () => deleteLine(), { timeout: 500 })
// Slow sequence - user has more time between keys
useHotkeySequence(['Shift+Z', 'Shift+Z'], () => forceQuit(), { timeout: 2000 })Controls whether the sequence is active. Defaults to .
Disabled sequences remain registered and stay visible in devtools; only execution is suppressed.
const [isVimMode, setIsVimMode] = useState(true)
useHotkeySequence(['G', 'G'], () => scrollToTop(), { enabled: isVimMode })const [isVimMode, setIsVimMode] = useState(true)
useHotkeySequence(['G', 'G'], () => scrollToTop(), { enabled: isVimMode })You can set default options for all calls by wrapping your component tree with . Per-hook options will override the provider defaults.
import { HotkeysProvider } from '@tanstack/react-hotkeys'
<HotkeysProvider
defaultOptions={{
hotkeySequence: { timeout: 1500 },
}}
>
<App />
</HotkeysProvider>import { HotkeysProvider } from '@tanstack/react-hotkeys'
<HotkeysProvider
defaultOptions={{
hotkeySequence: { timeout: 1500 },
}}
>
<App />
</HotkeysProvider>Sequences support the same option as hotkeys, allowing you to attach a and for use in shortcut palettes and devtools.
useHotkeySequence(['G', 'G'], () => scrollToTop(), {
meta: { name: 'Go to Top', description: 'Scroll to the top of the page' },
})useHotkeySequence(['G', 'G'], () => scrollToTop(), {
meta: { name: 'Go to Top', description: 'Scroll to the top of the page' },
})See the Hotkeys Guide for details on declaration merging and introspecting registrations.
Each step in a sequence can include modifiers:
// Ctrl+K followed by Ctrl+C (VS Code-style comment)
useHotkeySequence(['Mod+K', 'Mod+C'], () => {
commentSelection()
})
// g then Shift+G (go to bottom, Vim-style)
useHotkeySequence(['G', 'Shift+G'], () => {
scrollToBottom()
})// Ctrl+K followed by Ctrl+C (VS Code-style comment)
useHotkeySequence(['Mod+K', 'Mod+C'], () => {
commentSelection()
})
// g then Shift+G (go to bottom, Vim-style)
useHotkeySequence(['G', 'Shift+G'], () => {
scrollToBottom()
})You can repeat the same modifier across consecutive steps—for example then :
useHotkeySequence(['Shift+R', 'Shift+T'], () => {
doNextAction()
})useHotkeySequence(['Shift+R', 'Shift+T'], () => {
doNextAction()
})While a sequence is in progress, modifier-only keydown events (Shift, Control, Alt, or Meta pressed alone, with no letter or other key) are ignored. They do not advance the sequence and they do not reset progress. That way a user can tap Shift (or hold it) between chords such as and without breaking the sequence—similar to Vim-style flows where a modifier may be pressed before the next chord.
function VimNavigation() {
useHotkeySequence(['G', 'G'], () => scrollToTop())
useHotkeySequence(['G', 'Shift+G'], () => scrollToBottom())
useHotkeySequence(['D', 'D'], () => deleteLine())
useHotkeySequence(['D', 'W'], () => deleteWord())
useHotkeySequence(['C', 'I', 'W'], () => changeInnerWord())
}function VimNavigation() {
useHotkeySequence(['G', 'G'], () => scrollToTop())
useHotkeySequence(['G', 'Shift+G'], () => scrollToBottom())
useHotkeySequence(['D', 'D'], () => deleteLine())
useHotkeySequence(['D', 'W'], () => deleteWord())
useHotkeySequence(['C', 'I', 'W'], () => changeInnerWord())
}useHotkeySequence(
[
'ArrowUp', 'ArrowUp',
'ArrowDown', 'ArrowDown',
'ArrowLeft', 'ArrowRight',
'ArrowLeft', 'ArrowRight',
'B', 'A',
],
() => enableEasterEgg(),
{ timeout: 2000 },
)useHotkeySequence(
[
'ArrowUp', 'ArrowUp',
'ArrowDown', 'ArrowDown',
'ArrowLeft', 'ArrowRight',
'ArrowLeft', 'ArrowRight',
'B', 'A',
],
() => enableEasterEgg(),
{ timeout: 2000 },
)// Press "h", "e", "l", "p" to open help
useHotkeySequence(['H', 'E', 'L', 'P'], () => openHelp())// Press "h", "e", "l", "p" to open help
useHotkeySequence(['H', 'E', 'L', 'P'], () => openHelp())The (singleton) handles all sequence registrations. When a key is pressed:
Multiple sequences can share the same prefix. The manager tracks progress for each sequence independently:
// Both share the 'D' prefix
useHotkeySequence(['D', 'D'], () => deleteLine()) // dd
useHotkeySequence(['D', 'W'], () => deleteWord()) // dw
useHotkeySequence(['D', 'I', 'W'], () => deleteInnerWord()) // diw// Both share the 'D' prefix
useHotkeySequence(['D', 'D'], () => deleteLine()) // dd
useHotkeySequence(['D', 'W'], () => deleteWord()) // dw
useHotkeySequence(['D', 'I', 'W'], () => deleteInnerWord()) // diwAfter pressing , the manager waits for the next key to determine which sequence to complete.
Under the hood, uses the singleton . You can also use the core function for standalone sequence matching without the singleton:
import { createSequenceMatcher } from '@tanstack/react-hotkeys'
const matcher = createSequenceMatcher(['G', 'G'], {
timeout: 1000,
})
document.addEventListener('keydown', (e) => {
if (matcher.match(e)) {
console.log('Sequence completed!')
}
console.log('Progress:', matcher.getProgress()) // e.g., 1/2
})import { createSequenceMatcher } from '@tanstack/react-hotkeys'
const matcher = createSequenceMatcher(['G', 'G'], {
timeout: 1000,
})
document.addEventListener('keydown', (e) => {
if (matcher.match(e)) {
console.log('Sequence completed!')
}
console.log('Progress:', matcher.getProgress()) // e.g., 1/2
})