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 createHotkeySequence primitive to register a key sequence:
import { createHotkeySequence } from '@tanstack/solid-hotkeys'
function App() {
// Vim-style: press g then g to scroll to top
createHotkeySequence(['G', 'G'], () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
})
}import { createHotkeySequence } from '@tanstack/solid-hotkeys'
function App() {
// Vim-style: press g then g to scroll to top
createHotkeySequence(['G', 'G'], () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
})
}The first argument is an array of Hotkey strings representing each step in the sequence. The user must press them in order within the timeout window.
For several sequences or a dynamic list, use createHotkeySequences instead of many createHotkeySequence calls. Pass a plain array or an accessor that returns definitions.
import { createHotkeySequences } from '@tanstack/solid-hotkeys'
createHotkeySequences([
{ sequence: ['G', 'G'], callback: () => scrollToTop() },
{ sequence: ['D', 'D'], callback: () => deleteLine(), options: { timeout: 500 } },
])import { createHotkeySequences } from '@tanstack/solid-hotkeys'
createHotkeySequences([
{ sequence: ['G', 'G'], callback: () => scrollToTop() },
{ sequence: ['D', 'D'], callback: () => deleteLine(), options: { timeout: 500 } },
])Options merge like createHotkeys: HotkeysProvider defaults, then commonOptions, then each definition’s options. For element-scoped multi-sequence registration, use createHotkeySequencesAttachment.
Solid's createHotkeySequence accepts accessor functions for reactive sequence and options:
const [isVimMode, setIsVimMode] = createSignal(true)
const [sequence] = createSignal(['G', 'G'] as const)
createHotkeySequence(
sequence,
() => scrollToTop(),
() => ({ enabled: isVimMode(), timeout: 1500 }),
)const [isVimMode, setIsVimMode] = createSignal(true)
const [sequence] = createSignal(['G', 'G'] as const)
createHotkeySequence(
sequence,
() => scrollToTop(),
() => ({ enabled: isVimMode(), timeout: 1500 }),
)The third argument is an options object (or accessor returning options):
createHotkeySequence(['G', 'G'], callback, {
timeout: 1000, // Time allowed between keys (ms)
enabled: true, // Whether the sequence is active
target: document, // Or from an accessor for scoped sequences
})createHotkeySequence(['G', 'G'], callback, {
timeout: 1000, // Time allowed between keys (ms)
enabled: true, // Whether the sequence is active
target: document, // Or from an accessor for scoped sequences
})The maximum time (in milliseconds) allowed between consecutive key presses. Defaults to 1000 (1 second).
createHotkeySequence(['D', 'D'], () => deleteLine(), { timeout: 500 })
createHotkeySequence(['Shift+Z', 'Shift+Z'], () => forceQuit(), { timeout: 2000 })createHotkeySequence(['D', 'D'], () => deleteLine(), { timeout: 500 })
createHotkeySequence(['Shift+Z', 'Shift+Z'], () => forceQuit(), { timeout: 2000 })Controls whether the sequence is active. Defaults to true. Use an accessor for reactive control.
Disabled sequences remain registered and stay visible in devtools; only execution is suppressed.
const [isVimMode, setIsVimMode] = createSignal(true)
createHotkeySequence(['G', 'G'], () => scrollToTop(), () => ({
enabled: isVimMode(),
}))const [isVimMode, setIsVimMode] = createSignal(true)
createHotkeySequence(['G', 'G'], () => scrollToTop(), () => ({
enabled: isVimMode(),
}))The DOM element to attach the sequence listener to. Defaults to document. Can be from an accessor when the target becomes available after mount.
import { HotkeysProvider } from '@tanstack/solid-hotkeys'
<HotkeysProvider
defaultOptions={{
hotkeySequence: { timeout: 1500 },
}}
>
<App />
</HotkeysProvider>import { HotkeysProvider } from '@tanstack/solid-hotkeys'
<HotkeysProvider
defaultOptions={{
hotkeySequence: { timeout: 1500 },
}}
>
<App />
</HotkeysProvider>Sequences support the same meta option as hotkeys, allowing you to attach a name and description for use in shortcut palettes and devtools.
createHotkeySequence(['G', 'G'], () => scrollToTop(), {
meta: { name: 'Go to Top', description: 'Scroll to the top of the page' },
})createHotkeySequence(['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:
createHotkeySequence(['Mod+K', 'Mod+C'], () => commentSelection())
createHotkeySequence(['G', 'Shift+G'], () => scrollToBottom())createHotkeySequence(['Mod+K', 'Mod+C'], () => commentSelection())
createHotkeySequence(['G', 'Shift+G'], () => scrollToBottom())You can repeat the same modifier across consecutive steps—for example Shift+R then Shift+T:
createHotkeySequence(['Shift+R', 'Shift+T'], () => {
doNextAction()
})createHotkeySequence(['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, so a user can tap or hold Shift between chords without breaking the sequence.
function VimNavigation() {
createHotkeySequence(['G', 'G'], () => scrollToTop())
createHotkeySequence(['G', 'Shift+G'], () => scrollToBottom())
createHotkeySequence(['D', 'D'], () => deleteLine())
createHotkeySequence(['D', 'W'], () => deleteWord())
createHotkeySequence(['C', 'I', 'W'], () => changeInnerWord())
}function VimNavigation() {
createHotkeySequence(['G', 'G'], () => scrollToTop())
createHotkeySequence(['G', 'Shift+G'], () => scrollToBottom())
createHotkeySequence(['D', 'D'], () => deleteLine())
createHotkeySequence(['D', 'W'], () => deleteWord())
createHotkeySequence(['C', 'I', 'W'], () => changeInnerWord())
}createHotkeySequence(
[
'ArrowUp', 'ArrowUp',
'ArrowDown', 'ArrowDown',
'ArrowLeft', 'ArrowRight',
'ArrowLeft', 'ArrowRight',
'B', 'A',
],
() => enableEasterEgg(),
{ timeout: 2000 },
)createHotkeySequence(
[
'ArrowUp', 'ArrowUp',
'ArrowDown', 'ArrowDown',
'ArrowLeft', 'ArrowRight',
'ArrowLeft', 'ArrowRight',
'B', 'A',
],
() => enableEasterEgg(),
{ timeout: 2000 },
)createHotkeySequence(['H', 'E', 'L', 'P'], () => openHelp())createHotkeySequence(['H', 'E', 'L', 'P'], () => openHelp())The SequenceManager (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:
createHotkeySequence(['D', 'D'], () => deleteLine())
createHotkeySequence(['D', 'W'], () => deleteWord())
createHotkeySequence(['D', 'I', 'W'], () => deleteInnerWord())createHotkeySequence(['D', 'D'], () => deleteLine())
createHotkeySequence(['D', 'W'], () => deleteWord())
createHotkeySequence(['D', 'I', 'W'], () => deleteInnerWord())Under the hood, createHotkeySequence uses the singleton SequenceManager. You can also use the core createSequenceMatcher function for standalone sequence matching:
import { createSequenceMatcher } from '@tanstack/solid-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())
})import { createSequenceMatcher } from '@tanstack/solid-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())
})