Important
useHotkeySequence, useHotkeySequences, and useHotkeySequenceRecorder were removed in @tanstack/vue-hotkeys v0.14.0. Pass a sequence to useHotkey or useHotkeys instead, and record one with useHotkeyRecorder and maxSteps: Infinity.
A sequence is a hotkey made of chords pressed one after another, such as G then H to go home. Vim-style navigation, two-step commands such as VS Code's Ctrl+K then Ctrl+C, and cheat codes are all sequences.
Sequences are ordinary hotkeys. You register them with useHotkey and useHotkeys, and options, targets, enabled, and meta work as they do in Hotkeys. This guide covers what is different about sequences.
Pass an array of chords to useHotkey:
<script setup lang="ts">
import { useHotkey } from '@tanstack/vue-hotkeys'
// Press G, then H
useHotkey(['G', 'H'], () => goHome())
</script>useHotkeys registers chords and sequences in the same array:
useHotkeys([
{ hotkey: 'Mod+K', callback: () => openSearch() },
{ hotkey: ['G', 'H'], callback: () => goHome() },
{ hotkey: ['G', 'S'], callback: () => goToSettings() },
])The callback's context has kind: 'sequence', the registered hotkey array, and parsedSequence, with each step parsed:
useHotkey(['Mod+K', 'Mod+C'], (event, context) => {
console.log(context.hotkey) // ['Mod+K', 'Mod+C']
console.log(context.parsedSequence.map((step) => step.key)) // ['K', 'C']
})Each step is a chord, written the same way as any other chord:
useHotkey(['G', 'H'], () => goHome()) // The G key, then the H key, on any layout
useHotkey(['[KeyG]', '[KeyH]'], () => goHome()) // The G and H positions, whatever they type
useHotkey(['Mod+K', 'Mod+C'], () => commentLine()) // Command+K then Command+C on macOS, Control elsewhere
useHotkey(['G', 'Shift+G'], () => scrollToBottom()) // Each step has its own modifiers
useHotkey(['Shift+R', 'Shift+T'], () => runTests()) // Shift can stay held between steps
useHotkey([{ key: 'K', mod: true }, 'C'], () => openChat()) // A step can be a chord objectA one-step array, such as ['Escape'], registers as the chord 'Escape'. Write a chord as a string or an object covers chord syntax.
Each step must follow the previous one within timeout milliseconds, 1000 by default. Set it for one sequence:
useHotkey(['D', 'D'], () => deleteLine(), { timeout: 500 }) // A quick double press
useHotkey(['G', 'G'], () => scrollToTop(), { timeout: 2000 }) // More time between stepsTo change it for every sequence, set it in HotkeysProvider default options: :default-options="{ hotkey: { timeout: 1500 } }".
While the user works through a sequence:
Sequences allow 1 second between steps explains the default.
Sequences take the same options as chords, listed in Hotkey options, with these differences:
enabled, scope, and meta work as they do for chords. A disabled sequence ignores its steps.
Sequences can share their first steps. Each one tracks its own progress, and the next key decides which one completes:
useHotkeys([
{ hotkey: ['D', 'D'], callback: () => deleteLine() },
{ hotkey: ['D', 'W'], callback: () => deleteWord() },
{ hotkey: ['D', 'I', 'W'], callback: () => deleteInnerWord() },
])Sharing first steps isn't a duplicate, so TanStack Hotkeys doesn't warn. Two sequences with all the same steps are duplicates. See Handle duplicate hotkeys.
A key that continues a sequence belongs to the sequence, so a chord on the same key doesn't run:
useHotkeys([
{ hotkey: 'C', callback: () => compose() }, // C on its own
{ hotkey: ['G', 'C'], callback: () => goToContacts() }, // G, then C. compose() doesn't run.
])A sequence's first step doesn't belong to it yet, because the user may not be starting the sequence:
useHotkeys([
{ hotkey: 'G', callback: () => openGoMenu() }, // Runs on the first G
{ hotkey: ['G', 'G'], callback: () => scrollToTop() }, // Runs on the second G
])A sequence that has timed out, is disabled, or skips the focused text field doesn't claim the key.
The hotkey recorder and app hotkeys check conflicts more strictly. There, a hotkey conflicts with any sequence that starts with it, so G conflicts with ['G', 'H'], and ['G', 'H'] conflicts with ['G', 'H', 'J'].
formatForDisplay formats each step and joins them with a space:
formatForDisplay(['Mod+K', 'Mod+C'], { platform: 'mac' }) // '⌘ K ⌘ C'
formatForDisplay(['G', 'H'], { sequenceSeparator: ' then ' }) // 'G then H'Formatting & Display covers labels and keycaps.
A sequence's first steps have no visible effect, so show users what they've pressed so far, as VS Code does after Ctrl+K. Registration views report each sequence's progress in matchedStepCount:
<script setup lang="ts">
import { computed } from 'vue'
import { formatForDisplay, useHotkeyRegistrations } from '@tanstack/vue-hotkeys'
import type { HotkeySequenceRegistrationView } from '@tanstack/vue-hotkeys'
const registrations = useHotkeyRegistrations()
const longest = computed(
() =>
registrations.value
.filter(
(reg): reg is HotkeySequenceRegistrationView =>
reg.kind === 'sequence' && reg.matchedStepCount > 0,
)
.sort((a, b) => b.matchedStepCount - a.matchedStepCount)[0],
)
</script>
<template>
<p v-if="longest" role="status">
<kbd>{{ formatForDisplay(longest.hotkey.slice(0, longest.matchedStepCount)) }}</kbd>
was pressed. Waiting for the next key.
</p>
</template>Progress clears when the sequence completes, when the user presses a key that doesn't continue it, and when the timeout passes, so the message goes away on its own. When several sequences are in progress, the one with the most matched steps shows the most of what the user pressed. Cheat Sheet UI Helpers covers listing hotkeys.
A sequence made only of letter, number, or symbol keys, such as G then H, counts as a character key shortcut for accessibility, just like a single-key hotkey. Speech input can type it by accident. Let users turn such sequences off with enabled, or change them with app hotkeys. See Single-key shortcuts need an off switch.
useHotkeys([
{ hotkey: ['G', 'G'], callback: () => scrollToTop() },
{ hotkey: ['G', 'Shift+G'], callback: () => scrollToBottom() },
{ hotkey: ['D', 'D'], callback: () => deleteLine() },
{ hotkey: ['D', 'W'], callback: () => deleteWord() },
{ hotkey: ['C', 'I', 'W'], callback: () => changeInnerWord() },
])useHotkey(
[
'ArrowUp', 'ArrowUp',
'ArrowDown', 'ArrowDown',
'ArrowLeft', 'ArrowRight',
'ArrowLeft', 'ArrowRight',
'B', 'A',
],
() => enableEasterEgg(),
{ timeout: 2000 },
)To record a sequence the user presses, see Custom Hotkey Recording. To match a sequence without the hotkey manager, see Match a sequence on your own.