diff --git a/.changeset/plural-sequences.md b/.changeset/plural-sequences.md
new file mode 100644
index 00000000..97685d35
--- /dev/null
+++ b/.changeset/plural-sequences.md
@@ -0,0 +1,10 @@
+---
+'@tanstack/react-hotkeys': minor
+'@tanstack/preact-hotkeys': minor
+'@tanstack/vue-hotkeys': minor
+'@tanstack/solid-hotkeys': minor
+'@tanstack/svelte-hotkeys': minor
+'@tanstack/angular-hotkeys': minor
+---
+
+Add plural sequence APIs (`useHotkeySequences`, `createHotkeySequences`, `createHotkeySequencesAttachment`, `injectHotkeySequences`) and align `enabled` across adapters: disabled registrations stay in the manager for devtools, only core dispatch is skipped, and toggling `enabled` updates handles via `setOptions` instead of churning unregister/register.
diff --git a/docs/config.json b/docs/config.json
index 022ba669..faeb1534 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -652,6 +652,14 @@
{
"label": "UseHotkeySequenceOptions",
"to": "framework/react/reference/interfaces/UseHotkeySequenceOptions"
+ },
+ {
+ "label": "useHotkeySequences",
+ "to": "framework/react/reference/functions/useHotkeySequences"
+ },
+ {
+ "label": "UseHotkeySequenceDefinition",
+ "to": "framework/react/reference/interfaces/UseHotkeySequenceDefinition"
}
]
},
@@ -665,6 +673,14 @@
{
"label": "UseHotkeySequenceOptions",
"to": "framework/preact/reference/interfaces/UseHotkeySequenceOptions"
+ },
+ {
+ "label": "useHotkeySequences",
+ "to": "framework/preact/reference/functions/useHotkeySequences"
+ },
+ {
+ "label": "UseHotkeySequenceDefinition",
+ "to": "framework/preact/reference/interfaces/UseHotkeySequenceDefinition"
}
]
},
@@ -678,6 +694,14 @@
{
"label": "CreateHotkeySequenceOptions",
"to": "framework/solid/reference/interfaces/CreateHotkeySequenceOptions"
+ },
+ {
+ "label": "createHotkeySequences",
+ "to": "framework/solid/reference/functions/createHotkeySequences"
+ },
+ {
+ "label": "CreateHotkeySequenceDefinition",
+ "to": "framework/solid/reference/interfaces/CreateHotkeySequenceDefinition"
}
]
},
@@ -691,6 +715,14 @@
{
"label": "InjectHotkeySequenceOptions",
"to": "framework/angular/reference/interfaces/InjectHotkeySequenceOptions"
+ },
+ {
+ "label": "injectHotkeySequences",
+ "to": "framework/angular/reference/functions/injectHotkeySequences"
+ },
+ {
+ "label": "InjectHotkeySequenceDefinition",
+ "to": "framework/angular/reference/interfaces/InjectHotkeySequenceDefinition"
}
]
},
@@ -704,6 +736,14 @@
{
"label": "UseHotkeySequenceOptions",
"to": "framework/vue/reference/interfaces/UseHotkeySequenceOptions"
+ },
+ {
+ "label": "useHotkeySequences",
+ "to": "framework/vue/reference/functions/useHotkeySequences"
+ },
+ {
+ "label": "UseHotkeySequenceDefinition",
+ "to": "framework/vue/reference/interfaces/UseHotkeySequenceDefinition"
}
]
},
@@ -721,6 +761,18 @@
{
"label": "CreateHotkeySequenceOptions",
"to": "framework/svelte/reference/interfaces/CreateHotkeySequenceOptions"
+ },
+ {
+ "label": "createHotkeySequences",
+ "to": "framework/svelte/reference/functions/createHotkeySequences"
+ },
+ {
+ "label": "createHotkeySequencesAttachment",
+ "to": "framework/svelte/reference/functions/createHotkeySequencesAttachment"
+ },
+ {
+ "label": "CreateHotkeySequenceDefinition",
+ "to": "framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition"
}
]
}
@@ -1200,6 +1252,10 @@
"label": "useHotkeySequence",
"to": "framework/react/examples/useHotkeySequence"
},
+ {
+ "label": "useHotkeySequences",
+ "to": "framework/react/examples/useHotkeySequences"
+ },
{
"label": "useHotkeyRecorder",
"to": "framework/react/examples/useHotkeyRecorder"
@@ -1233,6 +1289,10 @@
"label": "useHotkeySequence",
"to": "framework/preact/examples/useHotkeySequence"
},
+ {
+ "label": "useHotkeySequences",
+ "to": "framework/preact/examples/useHotkeySequences"
+ },
{
"label": "useHotkeyRecorder",
"to": "framework/preact/examples/useHotkeyRecorder"
@@ -1266,6 +1326,10 @@
"label": "createHotkeySequence",
"to": "framework/solid/examples/createHotkeySequence"
},
+ {
+ "label": "createHotkeySequences",
+ "to": "framework/solid/examples/createHotkeySequences"
+ },
{
"label": "createHotkeyRecorder",
"to": "framework/solid/examples/createHotkeyRecorder"
@@ -1299,6 +1363,10 @@
"label": "injectHotkeySequence",
"to": "framework/angular/examples/injectHotkeySequence"
},
+ {
+ "label": "injectHotkeySequences",
+ "to": "framework/angular/examples/injectHotkeySequences"
+ },
{
"label": "injectHotkeyRecorder",
"to": "framework/angular/examples/injectHotkeyRecorder"
@@ -1332,6 +1400,10 @@
"label": "useHotkeySequence",
"to": "framework/vue/examples/useHotkeySequence"
},
+ {
+ "label": "useHotkeySequences",
+ "to": "framework/vue/examples/useHotkeySequences"
+ },
{
"label": "useHotkeyRecorder",
"to": "framework/vue/examples/useHotkeyRecorder"
@@ -1365,6 +1437,10 @@
"label": "createHotkeySequence",
"to": "framework/svelte/examples/create-hotkey-sequence"
},
+ {
+ "label": "createHotkeySequences",
+ "to": "framework/svelte/examples/create-hotkey-sequences"
+ },
{
"label": "createHotkeyRecorder",
"to": "framework/svelte/examples/create-hotkey-recorder"
diff --git a/docs/framework/angular/guides/hotkeys.md b/docs/framework/angular/guides/hotkeys.md
index bafca47b..e772c94f 100644
--- a/docs/framework/angular/guides/hotkeys.md
+++ b/docs/framework/angular/guides/hotkeys.md
@@ -54,6 +54,8 @@ For reactive state, pass an accessor function as the third argument.
### `enabled`
+When `enabled` is false, the hotkey **stays registered** (visible in devtools); only the callback is suppressed.
+
```ts
import { Component, signal } from '@angular/core'
import { injectHotkey } from '@tanstack/angular-hotkeys'
diff --git a/docs/framework/angular/guides/sequences.md b/docs/framework/angular/guides/sequences.md
index 69e15466..80759a75 100644
--- a/docs/framework/angular/guides/sequences.md
+++ b/docs/framework/angular/guides/sequences.md
@@ -21,6 +21,35 @@ export class AppComponent {
}
```
+## Many sequences at once
+
+Use `injectHotkeySequences` when you want several sequences (or a list built from data) in one injection context, instead of many `injectHotkeySequence` calls.
+
+```ts
+import { Component } from '@angular/core'
+import { injectHotkeySequences } from '@tanstack/angular-hotkeys'
+
+@Component({ standalone: true, template: `` })
+export class AppComponent {
+ constructor() {
+ injectHotkeySequences([
+ {
+ sequence: ['G', 'G'],
+ callback: () =>
+ window.scrollTo({ top: 0, behavior: 'smooth' }),
+ },
+ {
+ sequence: ['D', 'D'],
+ callback: () => console.log('delete line'),
+ options: { timeout: 500 },
+ },
+ ])
+ }
+}
+```
+
+Options merge like `injectHotkeys`: `provideHotkeys` defaults, then `commonOptions`, then each definition’s `options`.
+
## Sequence Options
```ts
@@ -32,6 +61,8 @@ injectHotkeySequence(['G', 'G'], callback, {
### Reactive `enabled`
+When disabled, the sequence **stays registered** (visible in devtools); only execution is suppressed.
+
```ts
import { Component, signal } from '@angular/core'
import { injectHotkeySequence } from '@tanstack/angular-hotkeys'
diff --git a/docs/framework/angular/reference/functions/injectHotkey.md b/docs/framework/angular/reference/functions/injectHotkey.md
index b6f9ad80..c981ed63 100644
--- a/docs/framework/angular/reference/functions/injectHotkey.md
+++ b/docs/framework/angular/reference/functions/injectHotkey.md
@@ -12,7 +12,7 @@ function injectHotkey(
options): void;
```
-Defined in: [injectHotkey.ts:83](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkey.ts#L83)
+Defined in: [injectHotkey.ts:86](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkey.ts#L86)
Angular inject-based API for registering a keyboard hotkey.
@@ -23,6 +23,8 @@ containing the hotkey string and parsed hotkey.
Call in an injection context (e.g. constructor or field initializer).
Uses effect() to track reactive dependencies and update registration
when options or the callback change.
+`enabled: false` keeps the registration (visible in devtools) and only suppresses firing; the same
+handle is updated instead of unregistering and re-registering when identity is unchanged.
## Parameters
diff --git a/docs/framework/angular/reference/functions/injectHotkeySequence.md b/docs/framework/angular/reference/functions/injectHotkeySequence.md
index 31c97310..9e6dc675 100644
--- a/docs/framework/angular/reference/functions/injectHotkeySequence.md
+++ b/docs/framework/angular/reference/functions/injectHotkeySequence.md
@@ -12,7 +12,7 @@ function injectHotkeySequence(
options): void;
```
-Defined in: [injectHotkeySequence.ts:48](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequence.ts#L48)
+Defined in: [injectHotkeySequence.ts:58](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequence.ts#L58)
Angular inject-based API for registering a keyboard shortcut sequence (Vim-style).
@@ -40,7 +40,8 @@ Function to call when the sequence is completed
### options
-Options for the sequence behavior (or getter function)
+Options for the sequence behavior (or getter function). `enabled: false` still registers
+ the sequence (visible in devtools); only execution is suppressed.
[`InjectHotkeySequenceOptions`](../interfaces/InjectHotkeySequenceOptions.md) | () => [`InjectHotkeySequenceOptions`](../interfaces/InjectHotkeySequenceOptions.md)
diff --git a/docs/framework/angular/reference/functions/injectHotkeySequences.md b/docs/framework/angular/reference/functions/injectHotkeySequences.md
new file mode 100644
index 00000000..c4f43988
--- /dev/null
+++ b/docs/framework/angular/reference/functions/injectHotkeySequences.md
@@ -0,0 +1,55 @@
+---
+id: injectHotkeySequences
+title: injectHotkeySequences
+---
+
+# Function: injectHotkeySequences()
+
+```ts
+function injectHotkeySequences(sequences, commonOptions): void;
+```
+
+Defined in: [injectHotkeySequences.ts:51](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequences.ts#L51)
+
+Angular inject-based API for registering multiple keyboard shortcut sequences at once (Vim-style).
+
+Uses the singleton SequenceManager. Call in an injection context (e.g. constructor).
+Uses `effect()` to track reactive dependencies when definitions or options are getters.
+
+Options are merged in this order:
+provideHotkeys defaults < commonOptions < per-definition options
+
+Definitions with an empty `sequence` are skipped. Disabled sequences (`enabled: false`)
+remain registered so they stay visible in devtools; the core manager suppresses execution.
+
+## Parameters
+
+### sequences
+
+Array of sequence definitions, or getter returning them
+
+[`InjectHotkeySequenceDefinition`](../interfaces/InjectHotkeySequenceDefinition.md)[] | () => [`InjectHotkeySequenceDefinition`](../interfaces/InjectHotkeySequenceDefinition.md)[]
+
+### commonOptions
+
+Shared options for all sequences, or getter
+
+[`InjectHotkeySequenceOptions`](../interfaces/InjectHotkeySequenceOptions.md) | () => [`InjectHotkeySequenceOptions`](../interfaces/InjectHotkeySequenceOptions.md)
+
+## Returns
+
+`void`
+
+## Example
+
+```ts
+@Component({ ... })
+export class VimComponent {
+ constructor() {
+ injectHotkeySequences([
+ { sequence: ['G', 'G'], callback: () => this.goTop() },
+ { sequence: ['D', 'D'], callback: () => this.deleteLine() },
+ ])
+ }
+}
+```
diff --git a/docs/framework/angular/reference/index.md b/docs/framework/angular/reference/index.md
index f3102fed..c82e4f1d 100644
--- a/docs/framework/angular/reference/index.md
+++ b/docs/framework/angular/reference/index.md
@@ -13,6 +13,7 @@ title: "@tanstack/angular-hotkeys"
- [HotkeysProviderOptions](interfaces/HotkeysProviderOptions.md)
- [InjectHotkeyDefinition](interfaces/InjectHotkeyDefinition.md)
- [InjectHotkeyOptions](interfaces/InjectHotkeyOptions.md)
+- [InjectHotkeySequenceDefinition](interfaces/InjectHotkeySequenceDefinition.md)
- [InjectHotkeySequenceOptions](interfaces/InjectHotkeySequenceOptions.md)
## Variables
@@ -30,5 +31,6 @@ title: "@tanstack/angular-hotkeys"
- [injectHotkeysContext](functions/injectHotkeysContext.md)
- [injectHotkeySequence](functions/injectHotkeySequence.md)
- [injectHotkeySequenceRecorder](functions/injectHotkeySequenceRecorder.md)
+- [injectHotkeySequences](functions/injectHotkeySequences.md)
- [injectKeyHold](functions/injectKeyHold.md)
- [provideHotkeys](functions/provideHotkeys.md)
diff --git a/docs/framework/angular/reference/interfaces/InjectHotkeyOptions.md b/docs/framework/angular/reference/interfaces/InjectHotkeyOptions.md
index 548a7d43..a840c0b2 100644
--- a/docs/framework/angular/reference/interfaces/InjectHotkeyOptions.md
+++ b/docs/framework/angular/reference/interfaces/InjectHotkeyOptions.md
@@ -5,7 +5,7 @@ title: InjectHotkeyOptions
# Interface: InjectHotkeyOptions
-Defined in: [injectHotkey.ts:16](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkey.ts#L16)
+Defined in: [injectHotkey.ts:17](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkey.ts#L17)
## Extends
@@ -19,7 +19,7 @@ Defined in: [injectHotkey.ts:16](https://github.com/TanStack/hotkeys/blob/main/p
optional target: HTMLElement | Document | Window | null;
```
-Defined in: [injectHotkey.ts:24](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkey.ts#L24)
+Defined in: [injectHotkey.ts:25](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkey.ts#L25)
The DOM element to attach the event listener to.
Can be a direct DOM element, an accessor (for reactive targets that become
diff --git a/docs/framework/angular/reference/interfaces/InjectHotkeySequenceDefinition.md b/docs/framework/angular/reference/interfaces/InjectHotkeySequenceDefinition.md
new file mode 100644
index 00000000..77e35da7
--- /dev/null
+++ b/docs/framework/angular/reference/interfaces/InjectHotkeySequenceDefinition.md
@@ -0,0 +1,48 @@
+---
+id: InjectHotkeySequenceDefinition
+title: InjectHotkeySequenceDefinition
+---
+
+# Interface: InjectHotkeySequenceDefinition
+
+Defined in: [injectHotkeySequences.ts:14](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequences.ts#L14)
+
+A single sequence definition for use with `injectHotkeySequences`.
+
+## Properties
+
+### callback
+
+```ts
+callback: HotkeyCallback;
+```
+
+Defined in: [injectHotkeySequences.ts:18](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequences.ts#L18)
+
+The function to call when the sequence is completed
+
+***
+
+### options?
+
+```ts
+optional options:
+ | InjectHotkeySequenceOptions
+ | () => InjectHotkeySequenceOptions;
+```
+
+Defined in: [injectHotkeySequences.ts:20](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequences.ts#L20)
+
+Per-sequence options (merged on top of commonOptions)
+
+***
+
+### sequence
+
+```ts
+sequence: HotkeySequence | () => HotkeySequence;
+```
+
+Defined in: [injectHotkeySequences.ts:16](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequences.ts#L16)
+
+Array of hotkey strings that form the sequence
diff --git a/docs/framework/angular/reference/interfaces/InjectHotkeySequenceOptions.md b/docs/framework/angular/reference/interfaces/InjectHotkeySequenceOptions.md
index 61922e7b..e5663ee2 100644
--- a/docs/framework/angular/reference/interfaces/InjectHotkeySequenceOptions.md
+++ b/docs/framework/angular/reference/interfaces/InjectHotkeySequenceOptions.md
@@ -5,11 +5,11 @@ title: InjectHotkeySequenceOptions
# Interface: InjectHotkeySequenceOptions
-Defined in: [injectHotkeySequence.ts:10](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequence.ts#L10)
+Defined in: [injectHotkeySequence.ts:13](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequence.ts#L13)
## Extends
-- `Omit`\<`SequenceOptions`, `"enabled"`\>
+- `Omit`\<`SequenceOptions`, `"enabled"` \| `"target"`\>
## Properties
@@ -19,6 +19,20 @@ Defined in: [injectHotkeySequence.ts:10](https://github.com/TanStack/hotkeys/blo
optional enabled: boolean;
```
-Defined in: [injectHotkeySequence.ts:15](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequence.ts#L15)
+Defined in: [injectHotkeySequence.ts:18](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequence.ts#L18)
Whether the sequence is enabled. Defaults to true.
+
+***
+
+### target?
+
+```ts
+optional target: HTMLElement | Document | Window | null;
+```
+
+Defined in: [injectHotkeySequence.ts:24](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequence.ts#L24)
+
+The DOM element to attach the event listener to.
+Can be a direct DOM element, an accessor target, or null.
+Defaults to document when omitted.
diff --git a/docs/framework/preact/guides/hotkeys.md b/docs/framework/preact/guides/hotkeys.md
index 8859e640..a7ab75b7 100644
--- a/docs/framework/preact/guides/hotkeys.md
+++ b/docs/framework/preact/guides/hotkeys.md
@@ -91,6 +91,8 @@ import { HotkeysProvider } from '@tanstack/preact-hotkeys'
Controls whether the hotkey is active. Defaults to `true`.
+Disabled hotkeys **remain registered** in the manager and stay visible in devtools; only execution is suppressed. Hooks update `enabled` on the existing registration instead of unregistering and re-registering.
+
```tsx
const [isEditing, setIsEditing] = useState(false)
diff --git a/docs/framework/preact/guides/sequences.md b/docs/framework/preact/guides/sequences.md
index 0201ee9f..695e5080 100644
--- a/docs/framework/preact/guides/sequences.md
+++ b/docs/framework/preact/guides/sequences.md
@@ -22,6 +22,21 @@ function App() {
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.
+## Many sequences at once
+
+When you need several sequences—or a **dynamic** list whose length is not fixed at compile time—use `useHotkeySequences` instead of calling `useHotkeySequence` many times. One hook call keeps you within the rules of hooks while still registering every sequence.
+
+```tsx
+import { useHotkeySequences } from '@tanstack/preact-hotkeys'
+
+useHotkeySequences([
+ { sequence: ['G', 'G'], callback: () => scrollToTop() },
+ { sequence: ['D', 'D'], callback: () => deleteLine(), options: { timeout: 500 } },
+])
+```
+
+Options merge in the same order as `useHotkeys`: `HotkeysProvider` defaults, then the second-argument `commonOptions`, then each definition’s `options`.
+
## Sequence Options
The third argument is an options object:
@@ -50,6 +65,8 @@ useHotkeySequence(['Shift+Z', 'Shift+Z'], () => forceQuit(), { timeout: 2000 })
Controls whether the sequence is active. Defaults to `true`.
+Disabled sequences **remain registered** and stay visible in devtools; only execution is suppressed.
+
```tsx
const [isVimMode, setIsVimMode] = useState(true)
diff --git a/docs/framework/preact/reference/functions/useHotkey.md b/docs/framework/preact/reference/functions/useHotkey.md
index fb699e07..f24260b9 100644
--- a/docs/framework/preact/reference/functions/useHotkey.md
+++ b/docs/framework/preact/reference/functions/useHotkey.md
@@ -12,7 +12,7 @@ function useHotkey(
options): void;
```
-Defined in: [useHotkey.ts:91](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkey.ts#L91)
+Defined in: [useHotkey.ts:92](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkey.ts#L92)
Preact hook for registering a keyboard hotkey.
@@ -43,7 +43,8 @@ The function to call when the hotkey is pressed
[`UseHotkeyOptions`](../interfaces/UseHotkeyOptions.md) = `{}`
-Options for the hotkey behavior
+Options for the hotkey behavior. `enabled: false` keeps the registration (visible in devtools)
+ and only suppresses firing; the hook updates the existing handle instead of unregistering.
## Returns
diff --git a/docs/framework/preact/reference/functions/useHotkeySequence.md b/docs/framework/preact/reference/functions/useHotkeySequence.md
index 014c0474..348b9f5b 100644
--- a/docs/framework/preact/reference/functions/useHotkeySequence.md
+++ b/docs/framework/preact/reference/functions/useHotkeySequence.md
@@ -12,7 +12,7 @@ function useHotkeySequence(
options): void;
```
-Defined in: [useHotkeySequence.ts:73](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequence.ts#L73)
+Defined in: [useHotkeySequence.ts:74](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequence.ts#L74)
Preact hook for registering a keyboard shortcut sequence (Vim-style).
@@ -42,7 +42,8 @@ Function to call when the sequence is completed
[`UseHotkeySequenceOptions`](../interfaces/UseHotkeySequenceOptions.md) = `{}`
-Options for the sequence behavior
+Options for the sequence behavior. `enabled: false` keeps the registration (visible in devtools)
+ and only suppresses firing; the hook updates the existing handle instead of unregistering.
## Returns
diff --git a/docs/framework/preact/reference/functions/useHotkeySequences.md b/docs/framework/preact/reference/functions/useHotkeySequences.md
new file mode 100644
index 00000000..0820ffd9
--- /dev/null
+++ b/docs/framework/preact/reference/functions/useHotkeySequences.md
@@ -0,0 +1,70 @@
+---
+id: useHotkeySequences
+title: useHotkeySequences
+---
+
+# Function: useHotkeySequences()
+
+```ts
+function useHotkeySequences(definitions, commonOptions): void;
+```
+
+Defined in: [useHotkeySequences.ts:68](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequences.ts#L68)
+
+Preact hook for registering multiple keyboard shortcut sequences at once (Vim-style).
+
+Uses the singleton SequenceManager. Accepts a dynamic array of definitions so you can
+register variable-length lists without violating the rules of hooks.
+
+Options are merged in this order:
+HotkeysProvider defaults < commonOptions < per-definition options
+
+Callbacks and options are synced on every render to avoid stale closures.
+
+Definitions with an empty `sequence` are skipped (no registration).
+
+## Parameters
+
+### definitions
+
+[`UseHotkeySequenceDefinition`](../interfaces/UseHotkeySequenceDefinition.md)[]
+
+Array of sequence definitions to register
+
+### commonOptions
+
+[`UseHotkeySequenceOptions`](../interfaces/UseHotkeySequenceOptions.md) = `{}`
+
+Shared options applied to all sequences (overridden by per-definition options).
+ Per-row `enabled: false` still registers that sequence: `SequenceManager` suppresses execution only (the row
+ stays in the store and appears in TanStack Hotkeys devtools). Toggling `enabled` updates the existing handle
+ via `setOptions` (no unregister/re-register churn).
+
+## Returns
+
+`void`
+
+## Examples
+
+```tsx
+function VimPalette() {
+ useHotkeySequences([
+ { sequence: ['G', 'G'], callback: () => scrollToTop() },
+ { sequence: ['D', 'D'], callback: () => deleteLine() },
+ { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord(), options: { timeout: 500 } },
+ ])
+}
+```
+
+```tsx
+function DynamicSequences({ items }) {
+ useHotkeySequences(
+ items.map((item) => ({
+ sequence: item.chords,
+ callback: item.action,
+ options: { enabled: item.enabled },
+ })),
+ { preventDefault: true },
+ )
+}
+```
diff --git a/docs/framework/preact/reference/functions/useHotkeys.md b/docs/framework/preact/reference/functions/useHotkeys.md
index 7264e159..02d03d1d 100644
--- a/docs/framework/preact/reference/functions/useHotkeys.md
+++ b/docs/framework/preact/reference/functions/useHotkeys.md
@@ -9,7 +9,7 @@ title: useHotkeys
function useHotkeys(hotkeys, commonOptions): void;
```
-Defined in: [useHotkeys.ts:71](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeys.ts#L71)
+Defined in: [useHotkeys.ts:74](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeys.ts#L74)
Preact hook for registering multiple keyboard hotkeys at once.
@@ -34,7 +34,10 @@ Array of hotkey definitions to register
[`UseHotkeyOptions`](../interfaces/UseHotkeyOptions.md) = `{}`
-Shared options applied to all hotkeys (overridden by per-definition options)
+Shared options applied to all hotkeys (overridden by per-definition options).
+ Per-row `enabled: false` still registers that hotkey: `HotkeyManager` suppresses execution only (the row
+ stays in the store and appears in TanStack Hotkeys devtools). Toggling `enabled` updates the existing handle
+ via `setOptions` (no unregister/re-register churn).
## Returns
diff --git a/docs/framework/preact/reference/index.md b/docs/framework/preact/reference/index.md
index b2d72b35..b899a692 100644
--- a/docs/framework/preact/reference/index.md
+++ b/docs/framework/preact/reference/index.md
@@ -13,6 +13,7 @@ title: "@tanstack/preact-hotkeys"
- [PreactHotkeySequenceRecorder](interfaces/PreactHotkeySequenceRecorder.md)
- [UseHotkeyDefinition](interfaces/UseHotkeyDefinition.md)
- [UseHotkeyOptions](interfaces/UseHotkeyOptions.md)
+- [UseHotkeySequenceDefinition](interfaces/UseHotkeySequenceDefinition.md)
- [UseHotkeySequenceOptions](interfaces/UseHotkeySequenceOptions.md)
## Functions
@@ -27,4 +28,5 @@ title: "@tanstack/preact-hotkeys"
- [useHotkeysContext](functions/useHotkeysContext.md)
- [useHotkeySequence](functions/useHotkeySequence.md)
- [useHotkeySequenceRecorder](functions/useHotkeySequenceRecorder.md)
+- [useHotkeySequences](functions/useHotkeySequences.md)
- [useKeyHold](functions/useKeyHold.md)
diff --git a/docs/framework/preact/reference/interfaces/UseHotkeySequenceDefinition.md b/docs/framework/preact/reference/interfaces/UseHotkeySequenceDefinition.md
new file mode 100644
index 00000000..9534c315
--- /dev/null
+++ b/docs/framework/preact/reference/interfaces/UseHotkeySequenceDefinition.md
@@ -0,0 +1,46 @@
+---
+id: UseHotkeySequenceDefinition
+title: UseHotkeySequenceDefinition
+---
+
+# Interface: UseHotkeySequenceDefinition
+
+Defined in: [useHotkeySequences.ts:15](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequences.ts#L15)
+
+A single sequence definition for use with `useHotkeySequences`.
+
+## Properties
+
+### callback
+
+```ts
+callback: HotkeyCallback;
+```
+
+Defined in: [useHotkeySequences.ts:19](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequences.ts#L19)
+
+The function to call when the sequence is completed
+
+***
+
+### options?
+
+```ts
+optional options: UseHotkeySequenceOptions;
+```
+
+Defined in: [useHotkeySequences.ts:21](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequences.ts#L21)
+
+Per-sequence options (merged on top of commonOptions)
+
+***
+
+### sequence
+
+```ts
+sequence: HotkeySequence;
+```
+
+Defined in: [useHotkeySequences.ts:17](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequences.ts#L17)
+
+Array of hotkey strings that form the sequence
diff --git a/docs/framework/react/guides/hotkeys.md b/docs/framework/react/guides/hotkeys.md
index 71fe5b75..3a0f5c88 100644
--- a/docs/framework/react/guides/hotkeys.md
+++ b/docs/framework/react/guides/hotkeys.md
@@ -91,6 +91,8 @@ import { HotkeysProvider } from '@tanstack/react-hotkeys'
Controls whether the hotkey is active. Defaults to `true`.
+Disabled hotkeys **remain registered** in the manager and stay visible in devtools; only execution is suppressed. Framework hooks update `enabled` on the existing registration instead of unregistering and re-registering.
+
```tsx
const [isEditing, setIsEditing] = useState(false)
diff --git a/docs/framework/react/guides/sequences.md b/docs/framework/react/guides/sequences.md
index 4f27b12b..3ebd8cec 100644
--- a/docs/framework/react/guides/sequences.md
+++ b/docs/framework/react/guides/sequences.md
@@ -22,6 +22,21 @@ function App() {
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.
+## Many sequences at once
+
+When you need several sequences—or a **dynamic** list whose length is not fixed at compile time—use `useHotkeySequences` instead of calling `useHotkeySequence` many times. One hook call keeps you within the rules of hooks while still registering every sequence.
+
+```tsx
+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 `useHotkeys`: `HotkeysProvider` defaults, then the second-argument `commonOptions`, then each definition’s `options`.
+
## Sequence Options
The third argument is an options object:
@@ -49,6 +64,8 @@ useHotkeySequence(['Shift+Z', 'Shift+Z'], () => forceQuit(), { timeout: 2000 })
Controls whether the sequence is active. Defaults to `true`.
+Disabled sequences **remain registered** and stay visible in devtools; only execution is suppressed.
+
```tsx
const [isVimMode, setIsVimMode] = useState(true)
diff --git a/docs/framework/react/reference/functions/useHotkey.md b/docs/framework/react/reference/functions/useHotkey.md
index 9da8d452..17a96595 100644
--- a/docs/framework/react/reference/functions/useHotkey.md
+++ b/docs/framework/react/reference/functions/useHotkey.md
@@ -12,7 +12,7 @@ function useHotkey(
options): void;
```
-Defined in: [useHotkey.ts:90](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkey.ts#L90)
+Defined in: [useHotkey.ts:91](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkey.ts#L91)
React hook for registering a keyboard hotkey.
@@ -43,7 +43,8 @@ The function to call when the hotkey is pressed
[`UseHotkeyOptions`](../interfaces/UseHotkeyOptions.md) = `{}`
-Options for the hotkey behavior
+Options for the hotkey behavior. `enabled: false` keeps the registration (visible in devtools)
+ and only suppresses firing; the hook updates the existing handle instead of unregistering.
## Returns
diff --git a/docs/framework/react/reference/functions/useHotkeySequence.md b/docs/framework/react/reference/functions/useHotkeySequence.md
index 3436b036..de36b75c 100644
--- a/docs/framework/react/reference/functions/useHotkeySequence.md
+++ b/docs/framework/react/reference/functions/useHotkeySequence.md
@@ -12,7 +12,7 @@ function useHotkeySequence(
options): void;
```
-Defined in: [useHotkeySequence.ts:72](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequence.ts#L72)
+Defined in: [useHotkeySequence.ts:73](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequence.ts#L73)
React hook for registering a keyboard shortcut sequence (Vim-style).
@@ -42,7 +42,8 @@ Function to call when the sequence is completed
[`UseHotkeySequenceOptions`](../interfaces/UseHotkeySequenceOptions.md) = `{}`
-Options for the sequence behavior
+Options for the sequence behavior. `enabled: false` keeps the registration (visible in devtools)
+ and only suppresses firing; the hook updates the existing handle instead of unregistering.
## Returns
diff --git a/docs/framework/react/reference/functions/useHotkeySequences.md b/docs/framework/react/reference/functions/useHotkeySequences.md
new file mode 100644
index 00000000..057d7015
--- /dev/null
+++ b/docs/framework/react/reference/functions/useHotkeySequences.md
@@ -0,0 +1,70 @@
+---
+id: useHotkeySequences
+title: useHotkeySequences
+---
+
+# Function: useHotkeySequences()
+
+```ts
+function useHotkeySequences(definitions, commonOptions): void;
+```
+
+Defined in: [useHotkeySequences.ts:68](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequences.ts#L68)
+
+React hook for registering multiple keyboard shortcut sequences at once (Vim-style).
+
+Uses the singleton SequenceManager. Accepts a dynamic array of definitions so you can
+register variable-length lists without violating the rules of hooks.
+
+Options are merged in this order:
+HotkeysProvider defaults < commonOptions < per-definition options
+
+Callbacks and options are synced on every render to avoid stale closures.
+
+Definitions with an empty `sequence` are skipped (no registration).
+
+## Parameters
+
+### definitions
+
+[`UseHotkeySequenceDefinition`](../interfaces/UseHotkeySequenceDefinition.md)[]
+
+Array of sequence definitions to register
+
+### commonOptions
+
+[`UseHotkeySequenceOptions`](../interfaces/UseHotkeySequenceOptions.md) = `{}`
+
+Shared options applied to all sequences (overridden by per-definition options).
+ Per-row `enabled: false` still registers that sequence: `SequenceManager` suppresses execution only (the row
+ stays in the store and appears in TanStack Hotkeys devtools). Toggling `enabled` updates the existing handle
+ via `setOptions` (no unregister/re-register churn).
+
+## Returns
+
+`void`
+
+## Examples
+
+```tsx
+function VimPalette() {
+ useHotkeySequences([
+ { sequence: ['G', 'G'], callback: () => scrollToTop() },
+ { sequence: ['D', 'D'], callback: () => deleteLine() },
+ { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord(), options: { timeout: 500 } },
+ ])
+}
+```
+
+```tsx
+function DynamicSequences({ items }) {
+ useHotkeySequences(
+ items.map((item) => ({
+ sequence: item.chords,
+ callback: item.action,
+ options: { enabled: item.enabled },
+ })),
+ { preventDefault: true },
+ )
+}
+```
diff --git a/docs/framework/react/reference/functions/useHotkeys.md b/docs/framework/react/reference/functions/useHotkeys.md
index 20d618d2..e83a6b05 100644
--- a/docs/framework/react/reference/functions/useHotkeys.md
+++ b/docs/framework/react/reference/functions/useHotkeys.md
@@ -9,7 +9,7 @@ title: useHotkeys
function useHotkeys(hotkeys, commonOptions): void;
```
-Defined in: [useHotkeys.ts:71](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeys.ts#L71)
+Defined in: [useHotkeys.ts:74](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeys.ts#L74)
React hook for registering multiple keyboard hotkeys at once.
@@ -34,7 +34,10 @@ Array of hotkey definitions to register
[`UseHotkeyOptions`](../interfaces/UseHotkeyOptions.md) = `{}`
-Shared options applied to all hotkeys (overridden by per-definition options)
+Shared options applied to all hotkeys (overridden by per-definition options).
+ Per-row `enabled: false` still registers that hotkey: `HotkeyManager` suppresses execution only (the row
+ stays in the store and appears in TanStack Hotkeys devtools). Toggling `enabled` updates the existing handle
+ via `setOptions` (no unregister/re-register churn).
## Returns
diff --git a/docs/framework/react/reference/index.md b/docs/framework/react/reference/index.md
index f91c21e0..d694c729 100644
--- a/docs/framework/react/reference/index.md
+++ b/docs/framework/react/reference/index.md
@@ -13,6 +13,7 @@ title: "@tanstack/react-hotkeys"
- [ReactHotkeySequenceRecorder](interfaces/ReactHotkeySequenceRecorder.md)
- [UseHotkeyDefinition](interfaces/UseHotkeyDefinition.md)
- [UseHotkeyOptions](interfaces/UseHotkeyOptions.md)
+- [UseHotkeySequenceDefinition](interfaces/UseHotkeySequenceDefinition.md)
- [UseHotkeySequenceOptions](interfaces/UseHotkeySequenceOptions.md)
## Functions
@@ -27,4 +28,5 @@ title: "@tanstack/react-hotkeys"
- [useHotkeysContext](functions/useHotkeysContext.md)
- [useHotkeySequence](functions/useHotkeySequence.md)
- [useHotkeySequenceRecorder](functions/useHotkeySequenceRecorder.md)
+- [useHotkeySequences](functions/useHotkeySequences.md)
- [useKeyHold](functions/useKeyHold.md)
diff --git a/docs/framework/react/reference/interfaces/UseHotkeySequenceDefinition.md b/docs/framework/react/reference/interfaces/UseHotkeySequenceDefinition.md
new file mode 100644
index 00000000..7965ef8d
--- /dev/null
+++ b/docs/framework/react/reference/interfaces/UseHotkeySequenceDefinition.md
@@ -0,0 +1,46 @@
+---
+id: UseHotkeySequenceDefinition
+title: UseHotkeySequenceDefinition
+---
+
+# Interface: UseHotkeySequenceDefinition
+
+Defined in: [useHotkeySequences.ts:15](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequences.ts#L15)
+
+A single sequence definition for use with `useHotkeySequences`.
+
+## Properties
+
+### callback
+
+```ts
+callback: HotkeyCallback;
+```
+
+Defined in: [useHotkeySequences.ts:19](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequences.ts#L19)
+
+The function to call when the sequence is completed
+
+***
+
+### options?
+
+```ts
+optional options: UseHotkeySequenceOptions;
+```
+
+Defined in: [useHotkeySequences.ts:21](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequences.ts#L21)
+
+Per-sequence options (merged on top of commonOptions)
+
+***
+
+### sequence
+
+```ts
+sequence: HotkeySequence;
+```
+
+Defined in: [useHotkeySequences.ts:17](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequences.ts#L17)
+
+Array of hotkey strings that form the sequence
diff --git a/docs/framework/solid/guides/hotkeys.md b/docs/framework/solid/guides/hotkeys.md
index f5c85d3b..93fdb99b 100644
--- a/docs/framework/solid/guides/hotkeys.md
+++ b/docs/framework/solid/guides/hotkeys.md
@@ -120,6 +120,8 @@ import { HotkeysProvider } from '@tanstack/solid-hotkeys'
Controls whether the hotkey is active. Defaults to `true`. Use an accessor for reactive control.
+Disabled hotkeys **remain registered** in the manager and stay visible in devtools; only execution is suppressed.
+
```tsx
const [isEditing, setIsEditing] = createSignal(false)
diff --git a/docs/framework/solid/guides/sequences.md b/docs/framework/solid/guides/sequences.md
index da383da8..25edda67 100644
--- a/docs/framework/solid/guides/sequences.md
+++ b/docs/framework/solid/guides/sequences.md
@@ -22,6 +22,21 @@ function App() {
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.
+## Many sequences at once
+
+For several sequences or a **dynamic** list, use `createHotkeySequences` instead of many `createHotkeySequence` calls. Pass a plain array or an accessor that returns definitions.
+
+```tsx
+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`.
+
## Reactive Options
Solid's `createHotkeySequence` accepts **accessor functions** for reactive sequence and options:
@@ -62,6 +77,8 @@ 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.
+
```tsx
const [isVimMode, setIsVimMode] = createSignal(true)
diff --git a/docs/framework/solid/reference/functions/createHotkeySequences.md b/docs/framework/solid/reference/functions/createHotkeySequences.md
new file mode 100644
index 00000000..e92fd09a
--- /dev/null
+++ b/docs/framework/solid/reference/functions/createHotkeySequences.md
@@ -0,0 +1,64 @@
+---
+id: createHotkeySequences
+title: createHotkeySequences
+---
+
+# Function: createHotkeySequences()
+
+```ts
+function createHotkeySequences(sequences, commonOptions): void;
+```
+
+Defined in: [createHotkeySequences.ts:61](https://github.com/TanStack/hotkeys/blob/main/packages/solid-hotkeys/src/createHotkeySequences.ts#L61)
+
+SolidJS primitive for registering multiple keyboard shortcut sequences at once (Vim-style).
+
+Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or accessors,
+so you can react to variable-length lists.
+
+Options are merged in this order:
+HotkeysProvider defaults < commonOptions < per-definition options
+
+Definitions with an empty `sequence` are skipped (no registration).
+
+## Parameters
+
+### sequences
+
+Array of sequence definitions, or accessor returning them
+
+[`CreateHotkeySequenceDefinition`](../interfaces/CreateHotkeySequenceDefinition.md)[] | () => [`CreateHotkeySequenceDefinition`](../interfaces/CreateHotkeySequenceDefinition.md)[]
+
+### commonOptions
+
+Shared options for all sequences, or accessor
+
+[`CreateHotkeySequenceOptions`](../interfaces/CreateHotkeySequenceOptions.md) | () => [`CreateHotkeySequenceOptions`](../interfaces/CreateHotkeySequenceOptions.md)
+
+## Returns
+
+`void`
+
+## Examples
+
+```tsx
+function VimPalette() {
+ createHotkeySequences([
+ { sequence: ['G', 'G'], callback: () => scrollToTop() },
+ { sequence: ['D', 'D'], callback: () => deleteLine() },
+ ])
+}
+```
+
+```tsx
+function Dynamic(props) {
+ createHotkeySequences(
+ () => props.items.map((item) => ({
+ sequence: item.chords,
+ callback: item.action,
+ options: { enabled: item.enabled },
+ })),
+ { preventDefault: true },
+ )
+}
+```
diff --git a/docs/framework/solid/reference/index.md b/docs/framework/solid/reference/index.md
index 866d5a1a..425d2f0a 100644
--- a/docs/framework/solid/reference/index.md
+++ b/docs/framework/solid/reference/index.md
@@ -9,6 +9,7 @@ title: "@tanstack/solid-hotkeys"
- [CreateHotkeyDefinition](interfaces/CreateHotkeyDefinition.md)
- [CreateHotkeyOptions](interfaces/CreateHotkeyOptions.md)
+- [CreateHotkeySequenceDefinition](interfaces/CreateHotkeySequenceDefinition.md)
- [CreateHotkeySequenceOptions](interfaces/CreateHotkeySequenceOptions.md)
- [HotkeysProviderOptions](interfaces/HotkeysProviderOptions.md)
- [HotkeysProviderProps](interfaces/HotkeysProviderProps.md)
@@ -28,6 +29,7 @@ title: "@tanstack/solid-hotkeys"
- [createHotkeys](functions/createHotkeys.md)
- [createHotkeySequence](functions/createHotkeySequence.md)
- [createHotkeySequenceRecorder](functions/createHotkeySequenceRecorder.md)
+- [createHotkeySequences](functions/createHotkeySequences.md)
- [createKeyHold](functions/createKeyHold.md)
- [useDefaultHotkeysOptions](functions/useDefaultHotkeysOptions.md)
- [useHotkeysContext](functions/useHotkeysContext.md)
diff --git a/docs/framework/solid/reference/interfaces/CreateHotkeySequenceDefinition.md b/docs/framework/solid/reference/interfaces/CreateHotkeySequenceDefinition.md
new file mode 100644
index 00000000..d5d4ab66
--- /dev/null
+++ b/docs/framework/solid/reference/interfaces/CreateHotkeySequenceDefinition.md
@@ -0,0 +1,46 @@
+---
+id: CreateHotkeySequenceDefinition
+title: CreateHotkeySequenceDefinition
+---
+
+# Interface: CreateHotkeySequenceDefinition
+
+Defined in: [createHotkeySequences.ts:14](https://github.com/TanStack/hotkeys/blob/main/packages/solid-hotkeys/src/createHotkeySequences.ts#L14)
+
+A single sequence definition for use with `createHotkeySequences`.
+
+## Properties
+
+### callback
+
+```ts
+callback: HotkeyCallback;
+```
+
+Defined in: [createHotkeySequences.ts:18](https://github.com/TanStack/hotkeys/blob/main/packages/solid-hotkeys/src/createHotkeySequences.ts#L18)
+
+The function to call when the sequence is completed
+
+***
+
+### options?
+
+```ts
+optional options: CreateHotkeySequenceOptions;
+```
+
+Defined in: [createHotkeySequences.ts:20](https://github.com/TanStack/hotkeys/blob/main/packages/solid-hotkeys/src/createHotkeySequences.ts#L20)
+
+Per-sequence options (merged on top of commonOptions)
+
+***
+
+### sequence
+
+```ts
+sequence: HotkeySequence;
+```
+
+Defined in: [createHotkeySequences.ts:16](https://github.com/TanStack/hotkeys/blob/main/packages/solid-hotkeys/src/createHotkeySequences.ts#L16)
+
+Array of hotkey strings that form the sequence
diff --git a/docs/framework/svelte/guides/hotkeys.md b/docs/framework/svelte/guides/hotkeys.md
index 78eaed3d..bf23333f 100644
--- a/docs/framework/svelte/guides/hotkeys.md
+++ b/docs/framework/svelte/guides/hotkeys.md
@@ -48,6 +48,8 @@ Hotkeys can take plain values for static registrations or getter functions when
### Reactive `enabled`
+When `enabled` is false, the hotkey **stays registered** (visible in devtools); only the callback is suppressed.
+
```svelte
```
+## Many sequences at once
+
+Use `createHotkeySequences` to register several global sequences in one place (including from a reactive getter). For multiple sequences on a focused element, use `createHotkeySequencesAttachment` the same way you would use `createHotkeySequenceAttachment`.
+
+```svelte
+
+```
+
## Scoped sequences
Use `createHotkeySequenceAttachment` when a sequence should only be active while a specific element owns focus.
@@ -46,6 +61,8 @@ createHotkeySequence(['G', 'G'], callback, {
### Reactive `enabled`
+When disabled, the sequence **stays registered** (visible in devtools); only execution is suppressed.
+
```svelte
+```
diff --git a/docs/framework/svelte/reference/functions/createHotkeySequencesAttachment.md b/docs/framework/svelte/reference/functions/createHotkeySequencesAttachment.md
new file mode 100644
index 00000000..d843bb45
--- /dev/null
+++ b/docs/framework/svelte/reference/functions/createHotkeySequencesAttachment.md
@@ -0,0 +1,43 @@
+---
+id: createHotkeySequencesAttachment
+title: createHotkeySequencesAttachment
+---
+
+# Function: createHotkeySequencesAttachment()
+
+```ts
+function createHotkeySequencesAttachment(definitions, commonOptions): Attachment;
+```
+
+Defined in: [packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:184](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts#L184)
+
+Create an attachment for element-scoped multi-sequence registration.
+
+## Parameters
+
+### definitions
+
+`MaybeGetter`\<[`CreateHotkeySequenceDefinition`](../interfaces/CreateHotkeySequenceDefinition.md)[]\>
+
+### commonOptions
+
+`MaybeGetter`\<[`CreateHotkeySequenceOptions`](../interfaces/CreateHotkeySequenceOptions.md)\> = `{}`
+
+## Returns
+
+`Attachment`\<`HTMLElement`\>
+
+## Example
+
+```svelte
+
+
+Editor
+```
diff --git a/docs/framework/svelte/reference/index.md b/docs/framework/svelte/reference/index.md
index 2905c01c..0d06d731 100644
--- a/docs/framework/svelte/reference/index.md
+++ b/docs/framework/svelte/reference/index.md
@@ -9,6 +9,7 @@ title: "@tanstack/svelte-hotkeys"
- [CreateHotkeyDefinition](interfaces/CreateHotkeyDefinition.md)
- [CreateHotkeyOptions](interfaces/CreateHotkeyOptions.md)
+- [CreateHotkeySequenceDefinition](interfaces/CreateHotkeySequenceDefinition.md)
- [CreateHotkeySequenceOptions](interfaces/CreateHotkeySequenceOptions.md)
- [HotkeysProviderOptions](interfaces/HotkeysProviderOptions.md)
- [HotkeysProviderProps](interfaces/HotkeysProviderProps.md)
@@ -37,6 +38,8 @@ title: "@tanstack/svelte-hotkeys"
- [createHotkeySequence](functions/createHotkeySequence.md)
- [createHotkeySequenceAttachment](functions/createHotkeySequenceAttachment.md)
- [createHotkeySequenceRecorder](functions/createHotkeySequenceRecorder.md)
+- [createHotkeySequences](functions/createHotkeySequences.md)
+- [createHotkeySequencesAttachment](functions/createHotkeySequencesAttachment.md)
- [getDefaultHotkeysOptions](functions/getDefaultHotkeysOptions.md)
- [getHeldKeyCodesMap](functions/getHeldKeyCodesMap.md)
- [getHeldKeys](functions/getHeldKeys.md)
diff --git a/docs/framework/svelte/reference/interfaces/CreateHotkeyOptions.md b/docs/framework/svelte/reference/interfaces/CreateHotkeyOptions.md
index 142abc93..8b0b628b 100644
--- a/docs/framework/svelte/reference/interfaces/CreateHotkeyOptions.md
+++ b/docs/framework/svelte/reference/interfaces/CreateHotkeyOptions.md
@@ -5,7 +5,7 @@ title: CreateHotkeyOptions
# Interface: CreateHotkeyOptions
-Defined in: [packages/svelte-hotkeys/src/createHotkey.svelte.ts:18](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkey.svelte.ts#L18)
+Defined in: [packages/svelte-hotkeys/src/createHotkey.svelte.ts:20](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkey.svelte.ts#L20)
## Extends
@@ -19,4 +19,4 @@ Defined in: [packages/svelte-hotkeys/src/createHotkey.svelte.ts:18](https://gith
optional target: Document | Window;
```
-Defined in: [packages/svelte-hotkeys/src/createHotkey.svelte.ts:19](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkey.svelte.ts#L19)
+Defined in: [packages/svelte-hotkeys/src/createHotkey.svelte.ts:21](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkey.svelte.ts#L21)
diff --git a/docs/framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition.md b/docs/framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition.md
new file mode 100644
index 00000000..7347a8b9
--- /dev/null
+++ b/docs/framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition.md
@@ -0,0 +1,46 @@
+---
+id: CreateHotkeySequenceDefinition
+title: CreateHotkeySequenceDefinition
+---
+
+# Interface: CreateHotkeySequenceDefinition
+
+Defined in: [packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:18](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts#L18)
+
+A single sequence definition for use with `createHotkeySequences`.
+
+## Properties
+
+### callback
+
+```ts
+callback: HotkeyCallback;
+```
+
+Defined in: [packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:22](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts#L22)
+
+The function to call when the sequence is completed
+
+***
+
+### options?
+
+```ts
+optional options: MaybeGetter;
+```
+
+Defined in: [packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:24](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts#L24)
+
+Per-sequence options (merged on top of commonOptions)
+
+***
+
+### sequence
+
+```ts
+sequence: MaybeGetter;
+```
+
+Defined in: [packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:20](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts#L20)
+
+Array of hotkey strings that form the sequence
diff --git a/docs/framework/svelte/reference/interfaces/CreateHotkeySequenceOptions.md b/docs/framework/svelte/reference/interfaces/CreateHotkeySequenceOptions.md
index e25631a4..a2e34667 100644
--- a/docs/framework/svelte/reference/interfaces/CreateHotkeySequenceOptions.md
+++ b/docs/framework/svelte/reference/interfaces/CreateHotkeySequenceOptions.md
@@ -5,7 +5,7 @@ title: CreateHotkeySequenceOptions
# Interface: CreateHotkeySequenceOptions
-Defined in: [packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts:12](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts#L12)
+Defined in: [packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts:14](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts#L14)
## Extends
@@ -19,4 +19,4 @@ Defined in: [packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts:12](http
optional target: Document | Window;
```
-Defined in: [packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts:16](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts#L16)
+Defined in: [packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts:18](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts#L18)
diff --git a/docs/framework/vue/guides/hotkeys.md b/docs/framework/vue/guides/hotkeys.md
index d8049b04..aa4a0401 100644
--- a/docs/framework/vue/guides/hotkeys.md
+++ b/docs/framework/vue/guides/hotkeys.md
@@ -50,6 +50,8 @@ Vue-specific options can be plain values, refs, or getters.
### `enabled`
+When `enabled` is false, the hotkey **stays registered** (visible in devtools); only the callback is suppressed.
+
```vue
```
+## Many sequences at once
+
+When you need several sequences—or a **reactive** list whose length changes—use `useHotkeySequences` instead of many `useHotkeySequence` calls. One composable registers every sequence safely.
+
+```vue
+
+```
+
+Options merge like `useHotkeys`: `HotkeysProvider` defaults, then `commonOptions`, then each definition’s `options`.
+
## Sequence Options
```ts
@@ -28,6 +45,8 @@ useHotkeySequence(['G', 'G'], callback, {
### Reactive `enabled`
+When disabled, the sequence **stays registered** (visible in devtools); only execution is suppressed.
+
```vue
```
+For several sequences or a list that changes at runtime, prefer a single `useHotkeySequences([...])` call (see the [Sequences guide](./guides/sequences.md#many-sequences-at-once)).
+
### Tracking Held Keys
```vue
diff --git a/docs/framework/vue/reference/functions/useHotkeySequences.md b/docs/framework/vue/reference/functions/useHotkeySequences.md
new file mode 100644
index 00000000..81d75a4f
--- /dev/null
+++ b/docs/framework/vue/reference/functions/useHotkeySequences.md
@@ -0,0 +1,70 @@
+---
+id: useHotkeySequences
+title: useHotkeySequences
+---
+
+# Function: useHotkeySequences()
+
+```ts
+function useHotkeySequences(definitions, commonOptions): void;
+```
+
+Defined in: [packages/vue-hotkeys/src/useHotkeySequences.ts:68](https://github.com/TanStack/hotkeys/blob/main/packages/vue-hotkeys/src/useHotkeySequences.ts#L68)
+
+Vue composable for registering multiple keyboard shortcut sequences at once (Vim-style).
+
+Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or a getter/ref
+that returns one, so you can register variable-length lists safely.
+
+Options are merged in this order:
+HotkeysProvider defaults < commonOptions < per-definition options
+
+Definitions with an empty `sequence` are skipped (no registration).
+
+## Parameters
+
+### definitions
+
+`MaybeRefOrGetter`\<[`UseHotkeySequenceDefinition`](../interfaces/UseHotkeySequenceDefinition.md)[]\>
+
+Array of sequence definitions, or a getter/ref
+
+### commonOptions
+
+`MaybeRefOrGetter`\<[`UseHotkeySequenceOptions`](../interfaces/UseHotkeySequenceOptions.md)\> = `{}`
+
+Shared options applied to all sequences (overridden by per-definition options)
+
+## Returns
+
+`void`
+
+## Examples
+
+```vue
+
+```
+
+```vue
+
+```
diff --git a/docs/framework/vue/reference/index.md b/docs/framework/vue/reference/index.md
index 7a4c7e9f..06fce76d 100644
--- a/docs/framework/vue/reference/index.md
+++ b/docs/framework/vue/reference/index.md
@@ -10,6 +10,7 @@ title: "@tanstack/vue-hotkeys"
- [HotkeysProviderOptions](interfaces/HotkeysProviderOptions.md)
- [UseHotkeyDefinition](interfaces/UseHotkeyDefinition.md)
- [UseHotkeyOptions](interfaces/UseHotkeyOptions.md)
+- [UseHotkeySequenceDefinition](interfaces/UseHotkeySequenceDefinition.md)
- [UseHotkeySequenceOptions](interfaces/UseHotkeySequenceOptions.md)
- [VueHotkeyRecorder](interfaces/VueHotkeyRecorder.md)
- [VueHotkeySequenceRecorder](interfaces/VueHotkeySequenceRecorder.md)
@@ -30,4 +31,5 @@ title: "@tanstack/vue-hotkeys"
- [useHotkeysContext](functions/useHotkeysContext.md)
- [useHotkeySequence](functions/useHotkeySequence.md)
- [useHotkeySequenceRecorder](functions/useHotkeySequenceRecorder.md)
+- [useHotkeySequences](functions/useHotkeySequences.md)
- [useKeyHold](functions/useKeyHold.md)
diff --git a/docs/framework/vue/reference/interfaces/UseHotkeySequenceDefinition.md b/docs/framework/vue/reference/interfaces/UseHotkeySequenceDefinition.md
new file mode 100644
index 00000000..669d1ead
--- /dev/null
+++ b/docs/framework/vue/reference/interfaces/UseHotkeySequenceDefinition.md
@@ -0,0 +1,46 @@
+---
+id: UseHotkeySequenceDefinition
+title: UseHotkeySequenceDefinition
+---
+
+# Interface: UseHotkeySequenceDefinition
+
+Defined in: [packages/vue-hotkeys/src/useHotkeySequences.ts:15](https://github.com/TanStack/hotkeys/blob/main/packages/vue-hotkeys/src/useHotkeySequences.ts#L15)
+
+A single sequence definition for use with `useHotkeySequences`.
+
+## Properties
+
+### callback
+
+```ts
+callback: HotkeyCallback;
+```
+
+Defined in: [packages/vue-hotkeys/src/useHotkeySequences.ts:19](https://github.com/TanStack/hotkeys/blob/main/packages/vue-hotkeys/src/useHotkeySequences.ts#L19)
+
+The function to call when the sequence is completed
+
+***
+
+### options?
+
+```ts
+optional options: MaybeRefOrGetter;
+```
+
+Defined in: [packages/vue-hotkeys/src/useHotkeySequences.ts:21](https://github.com/TanStack/hotkeys/blob/main/packages/vue-hotkeys/src/useHotkeySequences.ts#L21)
+
+Per-sequence options (merged on top of commonOptions)
+
+***
+
+### sequence
+
+```ts
+sequence: MaybeRefOrGetter;
+```
+
+Defined in: [packages/vue-hotkeys/src/useHotkeySequences.ts:17](https://github.com/TanStack/hotkeys/blob/main/packages/vue-hotkeys/src/useHotkeySequences.ts#L17)
+
+Array of hotkey strings that form the sequence
diff --git a/docs/reference/classes/HotkeyManager.md b/docs/reference/classes/HotkeyManager.md
index d88a5685..0e1ab64d 100644
--- a/docs/reference/classes/HotkeyManager.md
+++ b/docs/reference/classes/HotkeyManager.md
@@ -5,7 +5,7 @@ title: HotkeyManager
# Class: HotkeyManager
-Defined in: [hotkey-manager.ts:140](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L140)
+Defined in: [hotkey-manager.ts:144](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L144)
Singleton manager for hotkey registrations.
@@ -34,7 +34,7 @@ unregister()
readonly registrations: Store>;
```
-Defined in: [hotkey-manager.ts:162](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L162)
+Defined in: [hotkey-manager.ts:166](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L166)
The TanStack Store containing all hotkey registrations.
Use this to subscribe to registration changes or access current registrations.
@@ -63,7 +63,7 @@ for (const [id, reg] of manager.registrations.state) {
destroy(): void;
```
-Defined in: [hotkey-manager.ts:698](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L698)
+Defined in: [hotkey-manager.ts:702](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L702)
Destroys the manager and removes all listeners.
@@ -79,7 +79,7 @@ Destroys the manager and removes all listeners.
getRegistrationCount(): number;
```
-Defined in: [hotkey-manager.ts:669](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L669)
+Defined in: [hotkey-manager.ts:673](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L673)
Gets the number of registered hotkeys.
@@ -95,7 +95,7 @@ Gets the number of registered hotkeys.
isRegistered(hotkey, target?): boolean;
```
-Defined in: [hotkey-manager.ts:680](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L680)
+Defined in: [hotkey-manager.ts:684](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L684)
Checks if a specific hotkey is registered.
@@ -130,7 +130,7 @@ register(
options): HotkeyRegistrationHandle;
```
-Defined in: [hotkey-manager.ts:225](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L225)
+Defined in: [hotkey-manager.ts:229](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L229)
Registers a hotkey handler and returns a handle for updating the registration.
@@ -186,7 +186,7 @@ handle.unregister()
triggerRegistration(id): boolean;
```
-Defined in: [hotkey-manager.ts:633](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L633)
+Defined in: [hotkey-manager.ts:637](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L637)
Triggers a registration's callback programmatically from devtools.
Creates a synthetic KeyboardEvent and invokes the callback.
@@ -213,7 +213,7 @@ True if the registration was found and triggered
static getInstance(): HotkeyManager;
```
-Defined in: [hotkey-manager.ts:183](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L183)
+Defined in: [hotkey-manager.ts:187](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L187)
Gets the singleton instance of HotkeyManager.
@@ -229,7 +229,7 @@ Gets the singleton instance of HotkeyManager.
static resetInstance(): void;
```
-Defined in: [hotkey-manager.ts:193](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L193)
+Defined in: [hotkey-manager.ts:197](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L197)
Resets the singleton instance. Useful for testing.
diff --git a/docs/reference/functions/getHotkeyManager.md b/docs/reference/functions/getHotkeyManager.md
index 03969d35..fdd5e2a2 100644
--- a/docs/reference/functions/getHotkeyManager.md
+++ b/docs/reference/functions/getHotkeyManager.md
@@ -9,7 +9,7 @@ title: getHotkeyManager
function getHotkeyManager(): HotkeyManager;
```
-Defined in: [hotkey-manager.ts:714](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L714)
+Defined in: [hotkey-manager.ts:718](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L718)
Gets the singleton HotkeyManager instance.
Convenience function for accessing the manager.
diff --git a/docs/reference/interfaces/HotkeyOptions.md b/docs/reference/interfaces/HotkeyOptions.md
index d37e6194..87797a00 100644
--- a/docs/reference/interfaces/HotkeyOptions.md
+++ b/docs/reference/interfaces/HotkeyOptions.md
@@ -29,9 +29,11 @@ Behavior when this hotkey conflicts with an existing registration on the same ta
optional enabled: boolean;
```
-Defined in: [hotkey-manager.ts:31](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L31)
+Defined in: [hotkey-manager.ts:35](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L35)
-Whether the hotkey is enabled. Defaults to true
+Soft-disable: when `false`, the callback does not run but the registration
+stays in `HotkeyManager` (and in devtools). Toggling this should update the
+existing handle via `setOptions` rather than unregistering. Defaults to `true`.
***
@@ -41,7 +43,7 @@ Whether the hotkey is enabled. Defaults to true
optional eventType: "keydown" | "keyup";
```
-Defined in: [hotkey-manager.ts:33](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L33)
+Defined in: [hotkey-manager.ts:37](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L37)
The event type to listen for. Defaults to 'keydown'
@@ -53,7 +55,7 @@ The event type to listen for. Defaults to 'keydown'
optional ignoreInputs: boolean;
```
-Defined in: [hotkey-manager.ts:35](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L35)
+Defined in: [hotkey-manager.ts:39](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L39)
Whether to ignore hotkeys when keyboard events originate from input-like elements (text inputs, textarea, select, contenteditable — button-type inputs like type=button/submit/reset are not ignored). Defaults based on hotkey: true for single keys and Shift/Alt combos; false for Ctrl/Meta shortcuts and Escape
@@ -65,7 +67,7 @@ Whether to ignore hotkeys when keyboard events originate from input-like element
optional platform: "mac" | "windows" | "linux";
```
-Defined in: [hotkey-manager.ts:37](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L37)
+Defined in: [hotkey-manager.ts:41](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L41)
The target platform for resolving 'Mod'
@@ -77,7 +79,7 @@ The target platform for resolving 'Mod'
optional preventDefault: boolean;
```
-Defined in: [hotkey-manager.ts:39](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L39)
+Defined in: [hotkey-manager.ts:43](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L43)
Prevent the default browser action when the hotkey matches. Defaults to true
@@ -89,7 +91,7 @@ Prevent the default browser action when the hotkey matches. Defaults to true
optional requireReset: boolean;
```
-Defined in: [hotkey-manager.ts:41](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L41)
+Defined in: [hotkey-manager.ts:45](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L45)
If true, only trigger once until all keys are released. Default: false
@@ -101,7 +103,7 @@ If true, only trigger once until all keys are released. Default: false
optional stopPropagation: boolean;
```
-Defined in: [hotkey-manager.ts:43](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L43)
+Defined in: [hotkey-manager.ts:47](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L47)
Stop event propagation when the hotkey matches. Defaults to true
@@ -113,6 +115,6 @@ Stop event propagation when the hotkey matches. Defaults to true
optional target: HTMLElement | Document | Window | null;
```
-Defined in: [hotkey-manager.ts:45](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L45)
+Defined in: [hotkey-manager.ts:49](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L49)
The DOM element to attach the event listener to. Defaults to document.
diff --git a/docs/reference/interfaces/HotkeyRegistration.md b/docs/reference/interfaces/HotkeyRegistration.md
index 85b11c21..d8519e96 100644
--- a/docs/reference/interfaces/HotkeyRegistration.md
+++ b/docs/reference/interfaces/HotkeyRegistration.md
@@ -5,7 +5,7 @@ title: HotkeyRegistration
# Interface: HotkeyRegistration
-Defined in: [hotkey-manager.ts:51](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L51)
+Defined in: [hotkey-manager.ts:55](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L55)
A registered hotkey handler in the HotkeyManager.
@@ -17,7 +17,7 @@ A registered hotkey handler in the HotkeyManager.
callback: HotkeyCallback;
```
-Defined in: [hotkey-manager.ts:53](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L53)
+Defined in: [hotkey-manager.ts:57](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L57)
The callback to invoke
@@ -29,7 +29,7 @@ The callback to invoke
hasFired: boolean;
```
-Defined in: [hotkey-manager.ts:55](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L55)
+Defined in: [hotkey-manager.ts:59](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L59)
Whether this registration has fired and needs reset (for requireReset)
@@ -41,7 +41,7 @@ Whether this registration has fired and needs reset (for requireReset)
hotkey: Hotkey;
```
-Defined in: [hotkey-manager.ts:57](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L57)
+Defined in: [hotkey-manager.ts:61](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L61)
The original hotkey string
@@ -53,7 +53,7 @@ The original hotkey string
id: string;
```
-Defined in: [hotkey-manager.ts:59](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L59)
+Defined in: [hotkey-manager.ts:63](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L63)
Unique identifier for this registration
@@ -65,7 +65,7 @@ Unique identifier for this registration
options: HotkeyOptions;
```
-Defined in: [hotkey-manager.ts:61](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L61)
+Defined in: [hotkey-manager.ts:65](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L65)
Options for this registration
@@ -77,7 +77,7 @@ Options for this registration
parsedHotkey: ParsedHotkey;
```
-Defined in: [hotkey-manager.ts:63](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L63)
+Defined in: [hotkey-manager.ts:67](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L67)
The parsed hotkey
@@ -89,7 +89,7 @@ The parsed hotkey
target: HTMLElement | Document | Window;
```
-Defined in: [hotkey-manager.ts:65](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L65)
+Defined in: [hotkey-manager.ts:69](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L69)
The resolved target element for this registration
@@ -101,6 +101,6 @@ The resolved target element for this registration
triggerCount: number;
```
-Defined in: [hotkey-manager.ts:67](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L67)
+Defined in: [hotkey-manager.ts:71](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L71)
How many times this registration's callback has been triggered
diff --git a/docs/reference/interfaces/HotkeyRegistrationHandle.md b/docs/reference/interfaces/HotkeyRegistrationHandle.md
index 78dc5c4a..75ae9138 100644
--- a/docs/reference/interfaces/HotkeyRegistrationHandle.md
+++ b/docs/reference/interfaces/HotkeyRegistrationHandle.md
@@ -5,7 +5,7 @@ title: HotkeyRegistrationHandle
# Interface: HotkeyRegistrationHandle
-Defined in: [hotkey-manager.ts:93](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L93)
+Defined in: [hotkey-manager.ts:97](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L97)
A handle returned from HotkeyManager.register() that allows updating
the callback and options without re-registering the hotkey.
@@ -38,7 +38,7 @@ handle.unregister()
callback: HotkeyCallback;
```
-Defined in: [hotkey-manager.ts:98](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L98)
+Defined in: [hotkey-manager.ts:102](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L102)
The callback function. Can be set directly to update without re-registering.
This avoids stale closures when the callback references React state.
@@ -51,7 +51,7 @@ This avoids stale closures when the callback references React state.
readonly id: string;
```
-Defined in: [hotkey-manager.ts:100](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L100)
+Defined in: [hotkey-manager.ts:104](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L104)
Unique identifier for this registration
@@ -63,7 +63,7 @@ Unique identifier for this registration
readonly isActive: boolean;
```
-Defined in: [hotkey-manager.ts:102](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L102)
+Defined in: [hotkey-manager.ts:106](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L106)
Check if this registration is still active (not unregistered)
@@ -75,7 +75,7 @@ Check if this registration is still active (not unregistered)
setOptions: (options) => void;
```
-Defined in: [hotkey-manager.ts:107](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L107)
+Defined in: [hotkey-manager.ts:111](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L111)
Update options (merged with existing options).
Useful for updating `enabled`, `preventDefault`, etc. without re-registering.
@@ -98,7 +98,7 @@ Useful for updating `enabled`, `preventDefault`, etc. without re-registering.
unregister: () => void;
```
-Defined in: [hotkey-manager.ts:109](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L109)
+Defined in: [hotkey-manager.ts:113](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L113)
Unregister this hotkey
diff --git a/docs/reference/interfaces/SequenceOptions.md b/docs/reference/interfaces/SequenceOptions.md
index 72672b60..65108cbd 100644
--- a/docs/reference/interfaces/SequenceOptions.md
+++ b/docs/reference/interfaces/SequenceOptions.md
@@ -38,9 +38,11 @@ Behavior when this hotkey conflicts with an existing registration on the same ta
optional enabled: boolean;
```
-Defined in: [hotkey-manager.ts:31](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L31)
+Defined in: [hotkey-manager.ts:35](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L35)
-Whether the hotkey is enabled. Defaults to true
+Soft-disable: when `false`, the callback does not run but the registration
+stays in `HotkeyManager` (and in devtools). Toggling this should update the
+existing handle via `setOptions` rather than unregistering. Defaults to `true`.
#### Inherited from
@@ -54,7 +56,7 @@ Whether the hotkey is enabled. Defaults to true
optional eventType: "keydown" | "keyup";
```
-Defined in: [hotkey-manager.ts:33](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L33)
+Defined in: [hotkey-manager.ts:37](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L37)
The event type to listen for. Defaults to 'keydown'
@@ -70,7 +72,7 @@ The event type to listen for. Defaults to 'keydown'
optional ignoreInputs: boolean;
```
-Defined in: [hotkey-manager.ts:35](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L35)
+Defined in: [hotkey-manager.ts:39](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L39)
Whether to ignore hotkeys when keyboard events originate from input-like elements (text inputs, textarea, select, contenteditable — button-type inputs like type=button/submit/reset are not ignored). Defaults based on hotkey: true for single keys and Shift/Alt combos; false for Ctrl/Meta shortcuts and Escape
@@ -86,7 +88,7 @@ Whether to ignore hotkeys when keyboard events originate from input-like element
optional platform: "mac" | "windows" | "linux";
```
-Defined in: [hotkey-manager.ts:37](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L37)
+Defined in: [hotkey-manager.ts:41](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L41)
The target platform for resolving 'Mod'
@@ -102,7 +104,7 @@ The target platform for resolving 'Mod'
optional preventDefault: boolean;
```
-Defined in: [hotkey-manager.ts:39](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L39)
+Defined in: [hotkey-manager.ts:43](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L43)
Prevent the default browser action when the hotkey matches. Defaults to true
@@ -118,7 +120,7 @@ Prevent the default browser action when the hotkey matches. Defaults to true
optional stopPropagation: boolean;
```
-Defined in: [hotkey-manager.ts:43](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L43)
+Defined in: [hotkey-manager.ts:47](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L47)
Stop event propagation when the hotkey matches. Defaults to true
@@ -134,7 +136,7 @@ Stop event propagation when the hotkey matches. Defaults to true
optional target: HTMLElement | Document | Window | null;
```
-Defined in: [hotkey-manager.ts:45](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L45)
+Defined in: [hotkey-manager.ts:49](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/hotkey-manager.ts#L49)
The DOM element to attach the event listener to. Defaults to document.
diff --git a/examples/angular/injectHotkeySequence/src/app/app.component.html b/examples/angular/injectHotkeySequence/src/app/app.component.html
index 6ece3b11..993de560 100644
--- a/examples/angular/injectHotkeySequence/src/app/app.component.html
+++ b/examples/angular/injectHotkeySequence/src/app/app.component.html
@@ -65,6 +65,16 @@ Spell It Out
h e l l o
Type "hello" quickly
+
+ This sequence is
+ {{
+ helloSequenceEnabled() ? 'enabled' : 'disabled'
+ }} .
+
+
+ {{ helloSequenceEnabled() ? 'Disable' : 'Enable' }} sequence
+
diff --git a/examples/angular/injectHotkeySequence/src/app/app.component.ts b/examples/angular/injectHotkeySequence/src/app/app.component.ts
index 79e2650f..6ecbddeb 100644
--- a/examples/angular/injectHotkeySequence/src/app/app.component.ts
+++ b/examples/angular/injectHotkeySequence/src/app/app.component.ts
@@ -10,6 +10,7 @@ import { injectHotkey, injectHotkeySequence } from '@tanstack/angular-hotkeys'
export class AppComponent {
lastSequence = signal(null)
history = signal([])
+ readonly helloSequenceEnabled = signal(true)
constructor() {
const addToHistory = (action: string) => {
@@ -40,8 +41,10 @@ export class AppComponent {
{ timeout: 1500 },
)
- injectHotkeySequence(['H', 'E', 'L', 'L', 'O'], () =>
- addToHistory('hello → Hello World!'),
+ injectHotkeySequence(
+ ['H', 'E', 'L', 'L', 'O'],
+ () => addToHistory('hello → Hello World!'),
+ () => ({ enabled: this.helloSequenceEnabled() }),
)
injectHotkeySequence(['Shift+R', 'Shift+T'], () =>
@@ -57,4 +60,8 @@ export class AppComponent {
clearHistory(): void {
this.history.set([])
}
+
+ toggleHelloSequence(): void {
+ this.helloSequenceEnabled.update((enabled) => !enabled)
+ }
}
diff --git a/examples/angular/injectHotkeySequence/src/styles.css b/examples/angular/injectHotkeySequence/src/styles.css
index 3a0fd933..eefa6f8e 100644
--- a/examples/angular/injectHotkeySequence/src/styles.css
+++ b/examples/angular/injectHotkeySequence/src/styles.css
@@ -70,6 +70,13 @@ kbd {
margin: 0 0 12px;
font-size: 16px;
}
+.sequence-toggle-status {
+ font-size: 13px;
+ color: #555;
+ font-style: normal;
+ margin: 12px 0 10px;
+ line-height: 1.4;
+}
.hint {
font-size: 12px;
color: #888;
diff --git a/examples/angular/injectHotkeySequences/angular.json b/examples/angular/injectHotkeySequences/angular.json
new file mode 100644
index 00000000..441a7c9c
--- /dev/null
+++ b/examples/angular/injectHotkeySequences/angular.json
@@ -0,0 +1,63 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "injectHotkeySequences": {
+ "projectType": "application",
+ "schematics": {
+ "@schematics/angular:class": { "skipTests": true },
+ "@schematics/angular:component": { "skipTests": true }
+ },
+ "root": "",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:application",
+ "options": {
+ "outputPath": "dist/inject-hotkey-sequences",
+ "index": "src/index.html",
+ "browser": "src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "tsconfig.json",
+ "assets": [],
+ "styles": ["src/styles.css"],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kB",
+ "maximumError": "1MB"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "injectHotkeySequences:build:production"
+ },
+ "development": {
+ "buildTarget": "injectHotkeySequences:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ }
+ }
+ }
+ },
+ "cli": { "analytics": false }
+}
diff --git a/examples/angular/injectHotkeySequences/package.json b/examples/angular/injectHotkeySequences/package.json
new file mode 100644
index 00000000..b44eed2e
--- /dev/null
+++ b/examples/angular/injectHotkeySequences/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "inject-hotkey-sequences",
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve --port=3069",
+ "dev": "ng serve --port=3069",
+ "build": "ng build",
+ "watch": "ng build --watch --configuration development",
+ "test": "ng test"
+ },
+ "private": true,
+ "dependencies": {
+ "@angular/common": "^21.2.5",
+ "@angular/compiler": "^21.2.5",
+ "@angular/core": "^21.2.5",
+ "@angular/forms": "^21.2.5",
+ "@angular/platform-browser": "^21.2.5",
+ "@angular/platform-browser-dynamic": "^21.2.5",
+ "@angular/router": "^21.2.5",
+ "@tanstack/angular-hotkeys": "^0.6.0",
+ "rxjs": "~7.8.2",
+ "tslib": "^2.8.1",
+ "zone.js": "~0.16.1"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "^21.2.3",
+ "@angular/cli": "^21.2.3",
+ "@angular/compiler-cli": "^21.2.5",
+ "@types/jasmine": "~6.0.0",
+ "jasmine-core": "~6.1.0",
+ "karma": "~6.4.4",
+ "karma-chrome-launcher": "~3.2.0",
+ "karma-coverage": "~2.2.1",
+ "karma-jasmine": "~5.1.0",
+ "karma-jasmine-html-reporter": "~2.2.0",
+ "typescript": "5.9.3"
+ }
+}
diff --git a/examples/angular/injectHotkeySequences/src/app/app.component.css b/examples/angular/injectHotkeySequences/src/app/app.component.css
new file mode 100644
index 00000000..5d4e87f3
--- /dev/null
+++ b/examples/angular/injectHotkeySequences/src/app/app.component.css
@@ -0,0 +1,3 @@
+:host {
+ display: block;
+}
diff --git a/examples/angular/injectHotkeySequences/src/app/app.component.html b/examples/angular/injectHotkeySequences/src/app/app.component.html
new file mode 100644
index 00000000..11cd6be2
--- /dev/null
+++ b/examples/angular/injectHotkeySequences/src/app/app.component.html
@@ -0,0 +1,152 @@
+
+
+
+
+
+ Vim-Style Commands
+
+
+
+ Sequence
+ Action
+
+
+
+
+ g g
+ Go to top
+
+
+ G (Shift+G)
+ Go to bottom
+
+
+ d d
+ Delete line
+
+
+ y y
+ Yank (copy) line
+
+
+ d w
+ Delete word
+
+
+ c i w
+ Change inner word
+
+
+
+
+
+
+ Fun Sequences
+
+
+
Konami Code (Partial)
+
↑ ↑ ↓ ↓
+
Use arrow keys within 1.5 seconds
+
+
+
Side to Side
+
← → ← →
+
Arrow keys within 1.5 seconds
+
+
+
Spell It Out
+
+ h e l l o
+
+
Type "hello" quickly
+
+ This sequence is
+ {{
+ helloSequenceEnabled() ? 'enabled' : 'disabled'
+ }} .
+
+
+ {{ helloSequenceEnabled() ? 'Disable' : 'Enable' }} sequence
+
+
+
+
+
+ @if (lastSequence(); as seq) {
+
+ Triggered: {{ seq }}
+
+ }
+
+
+ Chained Shift+letter sequences
+
+ Each step is a chord: hold Shift and press a letter. You can
+ press Shift alone between steps—those modifier-only presses
+ do not reset progress, so the next chord still counts.
+
+
+
+
+ Sequence
+ Action
+
+
+
+
+
+ Shift +r then Shift +t
+
+ Chained Shift+letter (2 steps)
+
+
+
+
+
+
+ Usage
+
+import {{ '{' }} injectHotkeySequences {{
+ '}'
+ }} from '@tanstack/angular-hotkeys'
+
+// In constructor or injection context:
+injectHotkeySequences([
+ {{ '{' }} sequence: ['G', 'G'], callback: () => scrollToTop() {{ '}' }},
+ {{ '{' }}
+ sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
+ callback: () => activateCheatMode(),
+ options: {{ '{' }} timeout: 1500 {{ '}' }},
+ {{ '}' }},
+ {{ '{' }} sequence: ['C', 'I', 'W'], callback: () => changeInnerWord() {{
+ '}'
+ }},
+ {{ '{' }} sequence: ['Shift+R', 'Shift+T'], callback: () => doSomething() {{
+ '}'
+ }},
+])
+
+
+ @if (history().length > 0) {
+
+ History
+
+ @for (item of history(); track item) {
+ {{ item }}
+ }
+
+ Clear History
+
+ }
+
+ Press Escape to clear history
+
+
diff --git a/examples/angular/injectHotkeySequences/src/app/app.component.ts b/examples/angular/injectHotkeySequences/src/app/app.component.ts
new file mode 100644
index 00000000..0fd46821
--- /dev/null
+++ b/examples/angular/injectHotkeySequences/src/app/app.component.ts
@@ -0,0 +1,80 @@
+import { Component, signal } from '@angular/core'
+import { injectHotkey, injectHotkeySequences } from '@tanstack/angular-hotkeys'
+
+@Component({
+ selector: 'app-root',
+ standalone: true,
+ templateUrl: './app.component.html',
+ styleUrl: './app.component.css',
+})
+export class AppComponent {
+ lastSequence = signal(null)
+ history = signal>([])
+ readonly helloSequenceEnabled = signal(true)
+
+ constructor() {
+ const addToHistory = (action: string) => {
+ this.lastSequence.set(action)
+ this.history.update((h) => [...h.slice(-9), action])
+ }
+
+ injectHotkeySequences([
+ {
+ sequence: ['G', 'G'],
+ callback: () => addToHistory('gg → Go to top'),
+ },
+ {
+ sequence: ['Shift+G'],
+ callback: () => addToHistory('G → Go to bottom'),
+ },
+ {
+ sequence: ['D', 'D'],
+ callback: () => addToHistory('dd → Delete line'),
+ },
+ {
+ sequence: ['Y', 'Y'],
+ callback: () => addToHistory('yy → Yank (copy) line'),
+ },
+ {
+ sequence: ['D', 'W'],
+ callback: () => addToHistory('dw → Delete word'),
+ },
+ {
+ sequence: ['C', 'I', 'W'],
+ callback: () => addToHistory('ciw → Change inner word'),
+ },
+ {
+ sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
+ callback: () => addToHistory('↑↑↓↓ → Konami code (partial)'),
+ options: { timeout: 1500 },
+ },
+ {
+ sequence: ['ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight'],
+ callback: () => addToHistory('←→←→ → Side to side!'),
+ options: { timeout: 1500 },
+ },
+ {
+ sequence: ['H', 'E', 'L', 'L', 'O'],
+ callback: () => addToHistory('hello → Hello World!'),
+ options: () => ({ enabled: this.helloSequenceEnabled() }),
+ },
+ {
+ sequence: ['Shift+R', 'Shift+T'],
+ callback: () => addToHistory('⇧R ⇧T → Chained Shift+letter (2 steps)'),
+ },
+ ])
+
+ injectHotkey('Escape', () => {
+ this.lastSequence.set(null)
+ this.history.set([])
+ })
+ }
+
+ clearHistory(): void {
+ this.history.set([])
+ }
+
+ toggleHelloSequence(): void {
+ this.helloSequenceEnabled.update((enabled) => !enabled)
+ }
+}
diff --git a/examples/angular/injectHotkeySequences/src/app/app.config.ts b/examples/angular/injectHotkeySequences/src/app/app.config.ts
new file mode 100644
index 00000000..9a7d7e90
--- /dev/null
+++ b/examples/angular/injectHotkeySequences/src/app/app.config.ts
@@ -0,0 +1,10 @@
+import type { ApplicationConfig } from '@angular/core'
+import { provideZoneChangeDetection } from '@angular/core'
+import { provideHotkeys } from '@tanstack/angular-hotkeys'
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideZoneChangeDetection({ eventCoalescing: true }),
+ provideHotkeys({}),
+ ],
+}
diff --git a/examples/angular/injectHotkeySequences/src/index.html b/examples/angular/injectHotkeySequences/src/index.html
new file mode 100644
index 00000000..9e8f41e4
--- /dev/null
+++ b/examples/angular/injectHotkeySequences/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ injectHotkeySequences - TanStack Hotkeys Angular Example
+
+
+
+
+
+
diff --git a/examples/angular/injectHotkeySequences/src/main.ts b/examples/angular/injectHotkeySequences/src/main.ts
new file mode 100644
index 00000000..c3d8f9af
--- /dev/null
+++ b/examples/angular/injectHotkeySequences/src/main.ts
@@ -0,0 +1,5 @@
+import { bootstrapApplication } from '@angular/platform-browser'
+import { appConfig } from './app/app.config'
+import { AppComponent } from './app/app.component'
+
+bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err))
diff --git a/examples/angular/injectHotkeySequences/src/styles.css b/examples/angular/injectHotkeySequences/src/styles.css
new file mode 100644
index 00000000..fec91ce8
--- /dev/null
+++ b/examples/angular/injectHotkeySequences/src/styles.css
@@ -0,0 +1,123 @@
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+ font-family:
+ system-ui,
+ -apple-system,
+ sans-serif;
+ background: #f5f5f5;
+ color: #333;
+}
+.app {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+}
+header h1 {
+ margin: 0 0 10px;
+ color: #0066cc;
+}
+header p {
+ color: #666;
+ max-width: 500px;
+ margin: 0 auto;
+}
+.demo-section {
+ background: white;
+ border-radius: 12px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+.demo-section h2 {
+ margin: 0 0 16px;
+ font-size: 20px;
+}
+kbd {
+ background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%);
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ padding: 2px 8px;
+ font-family: monospace;
+ font-size: 13px;
+ margin-right: 4px;
+}
+.sequence-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+.sequence-table th,
+.sequence-table td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid #eee;
+}
+.fun-sequences {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+}
+.sequence-card {
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 16px;
+ text-align: center;
+}
+.sequence-card h3 {
+ margin: 0 0 12px;
+ font-size: 16px;
+}
+.sequence-toggle-status {
+ font-size: 13px;
+ color: #555;
+ font-style: normal;
+ margin: 12px 0 10px;
+ line-height: 1.4;
+}
+.hint {
+ font-size: 12px;
+ color: #888;
+ font-style: italic;
+}
+.info-box {
+ background: #e3f2fd;
+ border-radius: 8px;
+ padding: 16px 20px;
+ margin-bottom: 24px;
+ font-size: 18px;
+}
+.info-box.success {
+ background: #e8f5e9;
+ color: #2e7d32;
+}
+.code-block {
+ background: #1e1e1e;
+ color: #d4d4d4;
+ padding: 16px;
+ border-radius: 8px;
+ overflow-x: auto;
+ font-size: 13px;
+}
+.history-list {
+ list-style: none;
+ padding: 0;
+ margin: 0 0 16px;
+}
+.history-list li {
+ padding: 10px 14px;
+ background: #f0f0f0;
+ border-radius: 6px;
+ margin-bottom: 6px;
+ font-family: monospace;
+ font-size: 14px;
+}
+button {
+ background: #0066cc;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 6px;
+ cursor: pointer;
+}
diff --git a/examples/angular/injectHotkeySequences/tsconfig.json b/examples/angular/injectHotkeySequences/tsconfig.json
new file mode 100644
index 00000000..f12677ca
--- /dev/null
+++ b/examples/angular/injectHotkeySequences/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "lib": ["ES2022", "dom"],
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "experimentalDecorators": true,
+ "moduleResolution": "bundler",
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "ES2022",
+ "types": []
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"]
+}
diff --git a/examples/preact/useHotkeySequence/src/index.css b/examples/preact/useHotkeySequence/src/index.css
index 0fe2ade8..ea833042 100644
--- a/examples/preact/useHotkeySequence/src/index.css
+++ b/examples/preact/useHotkeySequence/src/index.css
@@ -83,6 +83,13 @@ kbd {
.sequence-card p {
margin: 0 0 8px;
}
+.sequence-toggle-status {
+ font-size: 13px;
+ color: #555;
+ font-style: normal;
+ margin: 12px 0 10px !important;
+ line-height: 1.4;
+}
.hint {
font-size: 12px;
color: #888;
diff --git a/examples/preact/useHotkeySequence/src/index.tsx b/examples/preact/useHotkeySequence/src/index.tsx
index cfe4e7c5..c33fd3a4 100644
--- a/examples/preact/useHotkeySequence/src/index.tsx
+++ b/examples/preact/useHotkeySequence/src/index.tsx
@@ -12,6 +12,7 @@ import './index.css'
function App() {
const [lastSequence, setLastSequence] = React.useState(null)
const [history, setHistory] = React.useState>([])
+ const [helloSequenceEnabled, setHelloSequenceEnabled] = React.useState(true)
const addToHistory = (action: string) => {
setLastSequence(action)
@@ -39,8 +40,10 @@ function App() {
{ timeout: 1500 },
)
- useHotkeySequence(['H', 'E', 'L', 'L', 'O'], () =>
- addToHistory('hello → Hello World!'),
+ useHotkeySequence(
+ ['H', 'E', 'L', 'L', 'O'],
+ () => addToHistory('hello → Hello World!'),
+ { enabled: helloSequenceEnabled },
)
useHotkeySequence(['Shift+R', 'Shift+T'], () =>
@@ -137,6 +140,17 @@ function App() {
h e l l o
Type "hello" quickly
+
+ This sequence is{' '}
+ {helloSequenceEnabled ? 'enabled' : 'disabled'}
+ .
+
+ setHelloSequenceEnabled((v) => !v)}
+ >
+ {helloSequenceEnabled ? 'Disable' : 'Enable'} sequence
+
diff --git a/examples/preact/useHotkeySequences/eslint.config.js b/examples/preact/useHotkeySequences/eslint.config.js
new file mode 100644
index 00000000..3fd4ac43
--- /dev/null
+++ b/examples/preact/useHotkeySequences/eslint.config.js
@@ -0,0 +1,11 @@
+// @ts-check
+
+import rootConfig from '../../../eslint.config.js'
+
+/** @type {import('eslint').Linter.Config[]} */
+export default [
+ {
+ ignores: ['eslint.config.js'],
+ },
+ ...rootConfig,
+]
diff --git a/examples/preact/useHotkeySequences/index.html b/examples/preact/useHotkeySequences/index.html
new file mode 100644
index 00000000..aae5f349
--- /dev/null
+++ b/examples/preact/useHotkeySequences/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ useHotkeySequences - TanStack Hotkeys Preact Example
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/examples/preact/useHotkeySequences/package.json b/examples/preact/useHotkeySequences/package.json
new file mode 100644
index 00000000..2a5658ea
--- /dev/null
+++ b/examples/preact/useHotkeySequences/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@tanstack/hotkeys-example-preact-use-hotkey-sequences",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3069",
+ "build": "vite build",
+ "preview": "vite preview",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "test:types": "tsc"
+ },
+ "dependencies": {
+ "@tanstack/preact-hotkeys": "^0.6.0",
+ "preact": "^10.29.0"
+ },
+ "devDependencies": {
+ "@preact/preset-vite": "^2.10.5",
+ "@tanstack/preact-devtools": "0.10.0",
+ "@tanstack/preact-hotkeys-devtools": "^0.5.0",
+ "typescript": "5.9.3",
+ "vite": "^8.0.1"
+ }
+}
diff --git a/examples/preact/useHotkeySequences/src/index.css b/examples/preact/useHotkeySequences/src/index.css
new file mode 100644
index 00000000..643465c3
--- /dev/null
+++ b/examples/preact/useHotkeySequences/src/index.css
@@ -0,0 +1,141 @@
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+ font-family:
+ system-ui,
+ -apple-system,
+ sans-serif;
+ background: #f5f5f5;
+ color: #333;
+}
+.app {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+}
+header {
+ text-align: center;
+ margin-bottom: 40px;
+}
+header h1 {
+ margin: 0 0 10px;
+ color: #0066cc;
+}
+header p {
+ color: #666;
+ max-width: 500px;
+ margin: 0 auto;
+}
+.demo-section {
+ background: white;
+ border-radius: 12px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+.demo-section h2 {
+ margin: 0 0 16px;
+ font-size: 20px;
+}
+kbd {
+ background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%);
+ border: 1px solid #ccc;
+ border-bottom-width: 2px;
+ border-radius: 4px;
+ padding: 2px 8px;
+ font-family: monospace;
+ font-size: 13px;
+ margin-right: 4px;
+}
+.sequence-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+.sequence-table th,
+.sequence-table td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid #eee;
+}
+.sequence-table th {
+ font-weight: 600;
+ color: #666;
+ font-size: 14px;
+}
+.fun-sequences {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+}
+.sequence-card {
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 16px;
+ text-align: center;
+}
+.sequence-card h3 {
+ margin: 0 0 12px;
+ font-size: 16px;
+}
+.sequence-card p {
+ margin: 0 0 8px;
+}
+.sequence-toggle-status {
+ font-size: 13px;
+ color: #555;
+ font-style: normal;
+ margin: 12px 0 10px !important;
+ line-height: 1.4;
+}
+.hint {
+ font-size: 12px;
+ color: #888;
+ font-style: italic;
+}
+.info-box {
+ background: #e3f2fd;
+ border-radius: 8px;
+ padding: 16px 20px;
+ margin-bottom: 24px;
+ font-size: 18px;
+}
+.info-box.success {
+ background: #e8f5e9;
+ color: #2e7d32;
+}
+.code-block {
+ background: #1e1e1e;
+ color: #d4d4d4;
+ padding: 16px;
+ border-radius: 8px;
+ overflow-x: auto;
+ font-size: 13px;
+ line-height: 1.5;
+}
+.history-list {
+ list-style: none;
+ padding: 0;
+ margin: 0 0 16px;
+}
+.history-list li {
+ padding: 10px 14px;
+ background: #f0f0f0;
+ border-radius: 6px;
+ margin-bottom: 6px;
+ font-family: monospace;
+ font-size: 14px;
+}
+button {
+ background: #0066cc;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+}
+button:hover {
+ background: #0052a3;
+}
diff --git a/examples/preact/useHotkeySequences/src/index.tsx b/examples/preact/useHotkeySequences/src/index.tsx
new file mode 100644
index 00000000..d97205c3
--- /dev/null
+++ b/examples/preact/useHotkeySequences/src/index.tsx
@@ -0,0 +1,270 @@
+import React from 'preact/compat'
+import { render } from 'preact'
+import {
+ HotkeysProvider,
+ useHotkey,
+ useHotkeySequences,
+} from '@tanstack/preact-hotkeys'
+import { hotkeysDevtoolsPlugin } from '@tanstack/preact-hotkeys-devtools'
+import { TanStackDevtools } from '@tanstack/preact-devtools'
+import './index.css'
+
+function App() {
+ const [lastSequence, setLastSequence] = React.useState(null)
+ const [history, setHistory] = React.useState>([])
+ const [helloSequenceEnabled, setHelloSequenceEnabled] = React.useState(true)
+
+ const addToHistory = (action: string) => {
+ setLastSequence(action)
+ setHistory((h) => [...h.slice(-9), action])
+ }
+
+ useHotkeySequences([
+ { sequence: ['G', 'G'], callback: () => addToHistory('gg → Go to top') },
+ {
+ sequence: ['Shift+G'],
+ callback: () => addToHistory('G → Go to bottom'),
+ },
+ { sequence: ['D', 'D'], callback: () => addToHistory('dd → Delete line') },
+ {
+ sequence: ['Y', 'Y'],
+ callback: () => addToHistory('yy → Yank (copy) line'),
+ },
+ { sequence: ['D', 'W'], callback: () => addToHistory('dw → Delete word') },
+ {
+ sequence: ['C', 'I', 'W'],
+ callback: () => addToHistory('ciw → Change inner word'),
+ },
+ {
+ sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
+ callback: () => addToHistory('↑↑↓↓ → Konami code (partial)'),
+ options: { timeout: 1500 },
+ },
+ {
+ sequence: ['ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight'],
+ callback: () => addToHistory('←→←→ → Side to side!'),
+ options: { timeout: 1500 },
+ },
+ {
+ sequence: ['H', 'E', 'L', 'L', 'O'],
+ callback: () => addToHistory('hello → Hello World!'),
+ options: { enabled: helloSequenceEnabled },
+ },
+ {
+ sequence: ['Shift+R', 'Shift+T'],
+ callback: () => addToHistory('⇧R ⇧T → Chained Shift+letter (2 steps)'),
+ },
+ ])
+
+ // Clear history with Escape
+ useHotkey('Escape', () => {
+ setLastSequence(null)
+ setHistory([])
+ })
+
+ return (
+
+
+
+
+
+ Vim-Style Commands
+
+
+
+ Sequence
+ Action
+
+
+
+
+
+ g g
+
+ Go to top
+
+
+
+ G (Shift+G)
+
+ Go to bottom
+
+
+
+ d d
+
+ Delete line
+
+
+
+ y y
+
+ Yank (copy) line
+
+
+
+ d w
+
+ Delete word
+
+
+
+ c i w
+
+ Change inner word
+
+
+
+
+
+
+ Fun Sequences
+
+
+
Konami Code (Partial)
+
+ ↑ ↑ ↓ ↓
+
+
Use arrow keys within 1.5 seconds
+
+
+
Side to Side
+
+ ← → ← →
+
+
Arrow keys within 1.5 seconds
+
+
+
Spell It Out
+
+ h e l l o
+
+
Type "hello" quickly
+
+ This sequence is{' '}
+ {helloSequenceEnabled ? 'enabled' : 'disabled'}
+ .
+
+
setHelloSequenceEnabled((v) => !v)}
+ >
+ {helloSequenceEnabled ? 'Disable' : 'Enable'} sequence
+
+
+
+
+
+ {lastSequence && (
+
+ Triggered: {lastSequence}
+
+ )}
+
+
+
+
+ Chained Shift+letter sequences
+
+ Each step is a chord: hold Shift and press a letter. You
+ can press Shift alone between steps—those modifier-only
+ presses do not reset progress, so the next chord still counts.
+
+
+
+
+ Sequence
+ Action
+
+
+
+
+
+ Shift +r then Shift +
+ t
+
+ Chained Shift+letter (2 steps)
+
+
+
+
+
+
+ Usage
+ {`import { useHotkeySequences } from '@tanstack/preact-hotkeys'
+
+function VimEditor() {
+ useHotkeySequences([
+ { sequence: ['G', 'G'], callback: () => scrollToTop() },
+ {
+ sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
+ callback: () => activateCheatMode(),
+ options: { timeout: 1500 },
+ },
+ { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord() },
+ { sequence: ['Shift+R', 'Shift+T'], callback: () => doSomething() },
+ ])
+}`}
+
+
+ {history.length > 0 && (
+
+ History
+
+ {history.map((item, i) => (
+ {item}
+ ))}
+
+ setHistory([])}>Clear History
+
+ )}
+
+
+ Press Escape to clear history
+
+
+
+ )
+}
+
+// TanStackDevtools as sibling of App to avoid Preact hook errors when hotkeys update state
+const devtoolsPlugins = [hotkeysDevtoolsPlugin()]
+
+render(
+ // optionally, provide default options to an optional HotkeysProvider
+
+
+
+ ,
+ document.getElementById('root')!,
+)
diff --git a/examples/preact/useHotkeySequences/tsconfig.json b/examples/preact/useHotkeySequences/tsconfig.json
new file mode 100644
index 00000000..faa3381b
--- /dev/null
+++ b/examples/preact/useHotkeySequences/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "jsxImportSource": "preact"
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/examples/preact/useHotkeySequences/vite.config.ts b/examples/preact/useHotkeySequences/vite.config.ts
new file mode 100644
index 00000000..bfe110c0
--- /dev/null
+++ b/examples/preact/useHotkeySequences/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import preact from '@preact/preset-vite'
+
+export default defineConfig({
+ plugins: [preact()],
+})
diff --git a/examples/react/useHotkeySequence/src/index.css b/examples/react/useHotkeySequence/src/index.css
index 69b749be..e3437f77 100644
--- a/examples/react/useHotkeySequence/src/index.css
+++ b/examples/react/useHotkeySequence/src/index.css
@@ -84,6 +84,13 @@ kbd {
.sequence-card p {
margin: 0 0 8px;
}
+.sequence-toggle-status {
+ font-size: 13px;
+ color: #555;
+ font-style: normal;
+ margin: 12px 0 10px !important;
+ line-height: 1.4;
+}
.hint {
font-size: 12px;
color: #888;
diff --git a/examples/react/useHotkeySequence/src/index.tsx b/examples/react/useHotkeySequence/src/index.tsx
index 228ccd47..410735e7 100644
--- a/examples/react/useHotkeySequence/src/index.tsx
+++ b/examples/react/useHotkeySequence/src/index.tsx
@@ -12,6 +12,7 @@ import './index.css'
function App() {
const [lastSequence, setLastSequence] = React.useState(null)
const [history, setHistory] = React.useState>([])
+ const [helloSequenceEnabled, setHelloSequenceEnabled] = React.useState(true)
const addToHistory = (action: string) => {
setLastSequence(action)
@@ -39,8 +40,10 @@ function App() {
{ timeout: 1500 },
)
- useHotkeySequence(['H', 'E', 'L', 'L', 'O'], () =>
- addToHistory('hello → Hello World!'),
+ useHotkeySequence(
+ ['H', 'E', 'L', 'L', 'O'],
+ () => addToHistory('hello → Hello World!'),
+ { enabled: helloSequenceEnabled },
)
useHotkeySequence(['Shift+R', 'Shift+T'], () =>
@@ -137,6 +140,17 @@ function App() {
h e l l o
Type "hello" quickly
+
+ This sequence is{' '}
+ {helloSequenceEnabled ? 'enabled' : 'disabled'}
+ .
+
+ setHelloSequenceEnabled((v) => !v)}
+ >
+ {helloSequenceEnabled ? 'Disable' : 'Enable'} sequence
+
diff --git a/examples/react/useHotkeySequences/eslint.config.js b/examples/react/useHotkeySequences/eslint.config.js
new file mode 100644
index 00000000..3fd4ac43
--- /dev/null
+++ b/examples/react/useHotkeySequences/eslint.config.js
@@ -0,0 +1,11 @@
+// @ts-check
+
+import rootConfig from '../../../eslint.config.js'
+
+/** @type {import('eslint').Linter.Config[]} */
+export default [
+ {
+ ignores: ['eslint.config.js'],
+ },
+ ...rootConfig,
+]
diff --git a/examples/react/useHotkeySequences/index.html b/examples/react/useHotkeySequences/index.html
new file mode 100644
index 00000000..0f57a386
--- /dev/null
+++ b/examples/react/useHotkeySequences/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ useHotkeySequences - TanStack Hotkeys React Example
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/examples/react/useHotkeySequences/package.json b/examples/react/useHotkeySequences/package.json
new file mode 100644
index 00000000..4a02f7f8
--- /dev/null
+++ b/examples/react/useHotkeySequences/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@tanstack/hotkeys-example-react-use-hotkey-sequences",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3069",
+ "build": "vite build",
+ "preview": "vite preview",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "test:types": "tsc"
+ },
+ "dependencies": {
+ "@tanstack/react-hotkeys": "^0.6.0",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4"
+ },
+ "devDependencies": {
+ "@tanstack/react-devtools": "0.10.0",
+ "@tanstack/react-hotkeys-devtools": "^0.5.0",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "typescript": "5.9.3",
+ "vite": "^8.0.1"
+ }
+}
diff --git a/examples/react/useHotkeySequences/src/index.css b/examples/react/useHotkeySequences/src/index.css
new file mode 100644
index 00000000..d051158b
--- /dev/null
+++ b/examples/react/useHotkeySequences/src/index.css
@@ -0,0 +1,165 @@
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+ font-family:
+ system-ui,
+ -apple-system,
+ sans-serif;
+ background: #f5f5f5;
+ color: #333;
+}
+.app {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+}
+header {
+ text-align: center;
+ margin-bottom: 40px;
+}
+header h1 {
+ margin: 0 0 10px;
+ color: #0066cc;
+}
+header p {
+ color: #666;
+ max-width: 500px;
+ margin: 0 auto;
+}
+
+.demo-section {
+ background: white;
+ border-radius: 12px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+.demo-section h2 {
+ margin: 0 0 16px;
+ font-size: 20px;
+}
+kbd {
+ background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%);
+ border: 1px solid #ccc;
+ border-bottom-width: 2px;
+ border-radius: 4px;
+ padding: 2px 8px;
+ font-family: monospace;
+ font-size: 13px;
+ margin-right: 4px;
+}
+.sequence-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+.sequence-table th,
+.sequence-table td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid #eee;
+}
+.sequence-table th {
+ font-weight: 600;
+ color: #666;
+ font-size: 14px;
+}
+.fun-sequences {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+}
+.sequence-card {
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 16px;
+ text-align: center;
+}
+.sequence-card h3 {
+ margin: 0 0 12px;
+ font-size: 16px;
+}
+.sequence-card p {
+ margin: 0 0 8px;
+}
+.sequence-toggle-status {
+ font-size: 13px;
+ color: #555;
+ font-style: normal;
+ margin: 12px 0 10px !important;
+ line-height: 1.4;
+}
+.hint {
+ font-size: 12px;
+ color: #888;
+ font-style: italic;
+}
+.info-box {
+ background: #e3f2fd;
+ border-radius: 8px;
+ padding: 16px 20px;
+ margin-bottom: 24px;
+ font-size: 18px;
+}
+.info-box.success {
+ background: #e8f5e9;
+ color: #2e7d32;
+}
+.code-block {
+ background: #1e1e1e;
+ color: #d4d4d4;
+ padding: 16px;
+ border-radius: 8px;
+ overflow-x: auto;
+ font-size: 13px;
+ line-height: 1.5;
+}
+.history-list {
+ list-style: none;
+ padding: 0;
+ margin: 0 0 16px;
+}
+.history-list li {
+ padding: 10px 14px;
+ background: #f0f0f0;
+ border-radius: 6px;
+ margin-bottom: 6px;
+ font-family: monospace;
+ font-size: 14px;
+}
+button {
+ background: #0066cc;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+}
+button:hover {
+ background: #0052a3;
+}
+
+.counter {
+ font-size: 18px;
+ font-weight: bold;
+ color: #0066cc;
+ margin: 12px 0;
+}
+
+.demo-input {
+ width: 100%;
+ max-width: 400px;
+ padding: 12px 16px;
+ font-size: 14px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ margin-top: 8px;
+}
+
+.demo-input:focus {
+ outline: 2px solid #0066cc;
+ outline-offset: 2px;
+ border-color: #0066cc;
+}
diff --git a/examples/react/useHotkeySequences/src/index.tsx b/examples/react/useHotkeySequences/src/index.tsx
new file mode 100644
index 00000000..4f301c11
--- /dev/null
+++ b/examples/react/useHotkeySequences/src/index.tsx
@@ -0,0 +1,267 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import {
+ HotkeysProvider,
+ useHotkey,
+ useHotkeySequences,
+} from '@tanstack/react-hotkeys'
+import { hotkeysDevtoolsPlugin } from '@tanstack/react-hotkeys-devtools'
+import { TanStackDevtools } from '@tanstack/react-devtools'
+import './index.css'
+
+function App() {
+ const [lastSequence, setLastSequence] = React.useState(null)
+ const [history, setHistory] = React.useState>([])
+ const [helloSequenceEnabled, setHelloSequenceEnabled] = React.useState(true)
+
+ const addToHistory = (action: string) => {
+ setLastSequence(action)
+ setHistory((h) => [...h.slice(-9), action])
+ }
+
+ useHotkeySequences([
+ { sequence: ['G', 'G'], callback: () => addToHistory('gg → Go to top') },
+ {
+ sequence: ['Shift+G'],
+ callback: () => addToHistory('G → Go to bottom'),
+ },
+ { sequence: ['D', 'D'], callback: () => addToHistory('dd → Delete line') },
+ {
+ sequence: ['Y', 'Y'],
+ callback: () => addToHistory('yy → Yank (copy) line'),
+ },
+ { sequence: ['D', 'W'], callback: () => addToHistory('dw → Delete word') },
+ {
+ sequence: ['C', 'I', 'W'],
+ callback: () => addToHistory('ciw → Change inner word'),
+ },
+ {
+ sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
+ callback: () => addToHistory('↑↑↓↓ → Konami code (partial)'),
+ options: { timeout: 1500 },
+ },
+ {
+ sequence: ['ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight'],
+ callback: () => addToHistory('←→←→ → Side to side!'),
+ options: { timeout: 1500 },
+ },
+ {
+ sequence: ['H', 'E', 'L', 'L', 'O'],
+ callback: () => addToHistory('hello → Hello World!'),
+ options: { enabled: helloSequenceEnabled },
+ },
+ {
+ sequence: ['Shift+R', 'Shift+T'],
+ callback: () => addToHistory('⇧R ⇧T → Chained Shift+letter (2 steps)'),
+ },
+ ])
+
+ // Clear history with Escape
+ useHotkey('Escape', () => {
+ setLastSequence(null)
+ setHistory([])
+ })
+
+ return (
+
+
+
+
+
+ Vim-Style Commands
+
+
+
+ Sequence
+ Action
+
+
+
+
+
+ g g
+
+ Go to top
+
+
+
+ G (Shift+G)
+
+ Go to bottom
+
+
+
+ d d
+
+ Delete line
+
+
+
+ y y
+
+ Yank (copy) line
+
+
+
+ d w
+
+ Delete word
+
+
+
+ c i w
+
+ Change inner word
+
+
+
+
+
+
+ Fun Sequences
+
+
+
Konami Code (Partial)
+
+ ↑ ↑ ↓ ↓
+
+
Use arrow keys within 1.5 seconds
+
+
+
Side to Side
+
+ ← → ← →
+
+
Arrow keys within 1.5 seconds
+
+
+
Spell It Out
+
+ h e l l o
+
+
Type "hello" quickly
+
+ This sequence is{' '}
+ {helloSequenceEnabled ? 'enabled' : 'disabled'}
+ .
+
+
setHelloSequenceEnabled((v) => !v)}
+ >
+ {helloSequenceEnabled ? 'Disable' : 'Enable'} sequence
+
+
+
+
+
+ {lastSequence && (
+
+ Triggered: {lastSequence}
+
+ )}
+
+
+
+
+ Chained Shift+letter sequences
+
+ Each step is a chord: hold Shift and press a letter. You
+ can press Shift alone between steps—those modifier-only
+ presses do not reset progress, so the next chord still counts.
+
+
+
+
+ Sequence
+ Action
+
+
+
+
+
+ Shift +r then Shift +
+ t
+
+ Chained Shift+letter (2 steps)
+
+
+
+
+
+
+ Usage
+ {`import { useHotkeySequences } from '@tanstack/react-hotkeys'
+
+function VimEditor() {
+ useHotkeySequences([
+ { sequence: ['G', 'G'], callback: () => scrollToTop() },
+ {
+ sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
+ callback: () => activateCheatMode(),
+ options: { timeout: 1500 },
+ },
+ { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord() },
+ { sequence: ['Shift+R', 'Shift+T'], callback: () => doSomething() },
+ ])
+}`}
+
+
+ {history.length > 0 && (
+
+ History
+
+ {history.map((item, i) => (
+ {item}
+ ))}
+
+ setHistory([])}>Clear History
+
+ )}
+
+
+ Press Escape to clear history
+
+
+
+
+
+ )
+}
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+ // optionally, provide default options to an optional HotkeysProvider
+
+
+ ,
+)
diff --git a/examples/react/useHotkeySequences/tsconfig.json b/examples/react/useHotkeySequences/tsconfig.json
new file mode 100644
index 00000000..a97ff8c1
--- /dev/null
+++ b/examples/react/useHotkeySequences/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/examples/react/useHotkeySequences/vite.config.ts b/examples/react/useHotkeySequences/vite.config.ts
new file mode 100644
index 00000000..9ffcc675
--- /dev/null
+++ b/examples/react/useHotkeySequences/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/examples/solid/createHotkeySequence/src/index.css b/examples/solid/createHotkeySequence/src/index.css
index bfce1a5b..a033679a 100644
--- a/examples/solid/createHotkeySequence/src/index.css
+++ b/examples/solid/createHotkeySequence/src/index.css
@@ -71,6 +71,13 @@ kbd {
margin: 0 0 12px;
font-size: 16px;
}
+.sequence-toggle-status {
+ font-size: 13px;
+ color: #555;
+ font-style: normal;
+ margin: 12px 0 10px;
+ line-height: 1.4;
+}
.hint {
font-size: 12px;
color: #888;
diff --git a/examples/solid/createHotkeySequence/src/index.tsx b/examples/solid/createHotkeySequence/src/index.tsx
index 739816ad..56d6d095 100644
--- a/examples/solid/createHotkeySequence/src/index.tsx
+++ b/examples/solid/createHotkeySequence/src/index.tsx
@@ -13,6 +13,7 @@ import './index.css'
function App() {
const [lastSequence, setLastSequence] = createSignal(null)
const [history, setHistory] = createSignal>([])
+ const [helloSequenceEnabled, setHelloSequenceEnabled] = createSignal(true)
const addToHistory = (action: string) => {
setLastSequence(action)
setHistory((h) => [...h.slice(-9), action])
@@ -39,8 +40,10 @@ function App() {
{ timeout: 1500 },
)
- createHotkeySequence(['H', 'E', 'L', 'L', 'O'], () =>
- addToHistory('hello → Hello World!'),
+ createHotkeySequence(
+ ['H', 'E', 'L', 'L', 'O'],
+ () => addToHistory('hello → Hello World!'),
+ () => ({ enabled: helloSequenceEnabled() }),
)
createHotkeySequence(['Shift+R', 'Shift+T'], () =>
@@ -136,6 +139,19 @@ function App() {
h e l l o
Type "hello" quickly
+
+ This sequence is{' '}
+
+ {helloSequenceEnabled() ? 'enabled' : 'disabled'}
+
+ .
+
+ setHelloSequenceEnabled(!helloSequenceEnabled())}
+ >
+ {helloSequenceEnabled() ? 'Disable' : 'Enable'} sequence
+
diff --git a/examples/solid/createHotkeySequences/index.html b/examples/solid/createHotkeySequences/index.html
new file mode 100644
index 00000000..96724de9
--- /dev/null
+++ b/examples/solid/createHotkeySequences/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ createHotkeySequences - TanStack Hotkeys Solid Example
+
+
+
+
+
+
diff --git a/examples/solid/createHotkeySequences/package.json b/examples/solid/createHotkeySequences/package.json
new file mode 100644
index 00000000..1c5f2479
--- /dev/null
+++ b/examples/solid/createHotkeySequences/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "@tanstack/hotkeys-example-solid-create-hotkey-sequences",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3069",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tanstack/solid-devtools": "0.8.0",
+ "@tanstack/solid-hotkeys": "^0.6.0",
+ "@tanstack/solid-hotkeys-devtools": "^0.5.0",
+ "solid-js": "^1.9.11"
+ },
+ "devDependencies": {
+ "vite": "^8.0.1",
+ "vite-plugin-solid": "^2.11.11"
+ }
+}
diff --git a/examples/solid/createHotkeySequences/src/index.css b/examples/solid/createHotkeySequences/src/index.css
new file mode 100644
index 00000000..aaa2453d
--- /dev/null
+++ b/examples/solid/createHotkeySequences/src/index.css
@@ -0,0 +1,147 @@
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+ font-family:
+ system-ui,
+ -apple-system,
+ sans-serif;
+ background: #f5f5f5;
+ color: #333;
+}
+.app {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+}
+header h1 {
+ margin: 0 0 10px;
+ color: #0066cc;
+}
+header p {
+ color: #666;
+ max-width: 500px;
+ margin: 0 auto;
+}
+
+.demo-section {
+ background: white;
+ border-radius: 12px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+.demo-section h2 {
+ margin: 0 0 16px;
+ font-size: 20px;
+}
+kbd {
+ background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%);
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ padding: 2px 8px;
+ font-family: monospace;
+ font-size: 13px;
+ margin-right: 4px;
+}
+.sequence-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+.sequence-table th,
+.sequence-table td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid #eee;
+}
+.fun-sequences {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+}
+.sequence-card {
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 16px;
+ text-align: center;
+}
+.sequence-card h3 {
+ margin: 0 0 12px;
+ font-size: 16px;
+}
+.sequence-toggle-status {
+ font-size: 13px;
+ color: #555;
+ font-style: normal;
+ margin: 12px 0 10px;
+ line-height: 1.4;
+}
+.hint {
+ font-size: 12px;
+ color: #888;
+ font-style: italic;
+}
+.info-box {
+ background: #e3f2fd;
+ border-radius: 8px;
+ padding: 16px 20px;
+ margin-bottom: 24px;
+ font-size: 18px;
+}
+.info-box.success {
+ background: #e8f5e9;
+ color: #2e7d32;
+}
+.code-block {
+ background: #1e1e1e;
+ color: #d4d4d4;
+ padding: 16px;
+ border-radius: 8px;
+ overflow-x: auto;
+ font-size: 13px;
+}
+.history-list {
+ list-style: none;
+ padding: 0;
+ margin: 0 0 16px;
+}
+.history-list li {
+ padding: 10px 14px;
+ background: #f0f0f0;
+ border-radius: 6px;
+ margin-bottom: 6px;
+ font-family: monospace;
+ font-size: 14px;
+}
+button {
+ background: #0066cc;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 6px;
+ cursor: pointer;
+}
+
+.counter {
+ font-size: 18px;
+ font-weight: bold;
+ color: #0066cc;
+ margin: 12px 0;
+}
+
+.demo-input {
+ width: 100%;
+ max-width: 400px;
+ padding: 12px 16px;
+ font-size: 14px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ margin-top: 8px;
+}
+
+.demo-input:focus {
+ outline: 2px solid #0066cc;
+ outline-offset: 2px;
+ border-color: #0066cc;
+}
diff --git a/examples/solid/createHotkeySequences/src/index.tsx b/examples/solid/createHotkeySequences/src/index.tsx
new file mode 100644
index 00000000..47298fe7
--- /dev/null
+++ b/examples/solid/createHotkeySequences/src/index.tsx
@@ -0,0 +1,270 @@
+/* @refresh reload */
+import { render } from 'solid-js/web'
+import { Show, createSignal } from 'solid-js'
+import {
+ HotkeysProvider,
+ createHotkey,
+ createHotkeySequences,
+} from '@tanstack/solid-hotkeys'
+import { hotkeysDevtoolsPlugin } from '@tanstack/solid-hotkeys-devtools'
+import { TanStackDevtools } from '@tanstack/solid-devtools'
+import './index.css'
+
+function App() {
+ const [lastSequence, setLastSequence] = createSignal(null)
+ const [history, setHistory] = createSignal>([])
+ const [helloSequenceEnabled, setHelloSequenceEnabled] = createSignal(true)
+ const addToHistory = (action: string) => {
+ setLastSequence(action)
+ setHistory((h) => [...h.slice(-9), action])
+ }
+
+ createHotkeySequences([
+ { sequence: ['G', 'G'], callback: () => addToHistory('gg → Go to top') },
+ {
+ sequence: ['Shift+G'],
+ callback: () => addToHistory('G → Go to bottom'),
+ },
+ { sequence: ['D', 'D'], callback: () => addToHistory('dd → Delete line') },
+ {
+ sequence: ['Y', 'Y'],
+ callback: () => addToHistory('yy → Yank (copy) line'),
+ },
+ { sequence: ['D', 'W'], callback: () => addToHistory('dw → Delete word') },
+ {
+ sequence: ['C', 'I', 'W'],
+ callback: () => addToHistory('ciw → Change inner word'),
+ },
+ {
+ sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
+ callback: () => addToHistory('↑↑↓↓ → Konami code (partial)'),
+ options: { timeout: 1500 },
+ },
+ {
+ sequence: ['ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight'],
+ callback: () => addToHistory('←→←→ → Side to side!'),
+ options: { timeout: 1500 },
+ },
+ {
+ sequence: ['H', 'E', 'L', 'L', 'O'],
+ callback: () => addToHistory('hello → Hello World!'),
+ options: {
+ get enabled() {
+ return helloSequenceEnabled()
+ },
+ },
+ },
+ {
+ sequence: ['Shift+R', 'Shift+T'],
+ callback: () => addToHistory('⇧R ⇧T → Chained Shift+letter (2 steps)'),
+ },
+ ])
+
+ createHotkey('Escape', () => {
+ setLastSequence(null)
+ setHistory([])
+ })
+
+ return (
+
+
+
+
+
+ Vim-Style Commands
+
+
+
+ Sequence
+ Action
+
+
+
+
+
+ g g
+
+ Go to top
+
+
+
+ G (Shift+G)
+
+ Go to bottom
+
+
+
+ d d
+
+ Delete line
+
+
+
+ y y
+
+ Yank (copy) line
+
+
+
+ d w
+
+ Delete word
+
+
+
+ c i w
+
+ Change inner word
+
+
+
+
+
+
+ Fun Sequences
+
+
+
Konami Code (Partial)
+
+ ↑ ↑ ↓ ↓
+
+
Use arrow keys within 1.5 seconds
+
+
+
Side to Side
+
+ ← → ← →
+
+
Arrow keys within 1.5 seconds
+
+
+
Spell It Out
+
+ h e l l o
+
+
Type "hello" quickly
+
+ This sequence is{' '}
+
+ {helloSequenceEnabled() ? 'enabled' : 'disabled'}
+
+ .
+
+
setHelloSequenceEnabled(!helloSequenceEnabled())}
+ >
+ {helloSequenceEnabled() ? 'Disable' : 'Enable'} sequence
+
+
+
+
+
+
+
+ Triggered: {lastSequence()}
+
+
+
+
+
+
+ Chained Shift+letter sequences
+
+ Each step is a chord: hold Shift and press a letter. You
+ can press Shift alone between steps—those modifier-only
+ presses do not reset progress, so the next chord still counts.
+
+
+
+
+ Sequence
+ Action
+
+
+
+
+
+ Shift +r then Shift +
+ t
+
+ Chained Shift+letter (2 steps)
+
+
+
+
+
+
+ Usage
+ {`import { createHotkeySequences } from '@tanstack/solid-hotkeys'
+
+function VimEditor() {
+ createHotkeySequences([
+ { sequence: ['G', 'G'], callback: () => scrollToTop() },
+ {
+ sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
+ callback: () => activateCheatMode(),
+ options: { timeout: 1500 },
+ },
+ { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord() },
+ { sequence: ['Shift+R', 'Shift+T'], callback: () => doSomething() },
+ ])
+}`}
+
+
+ 0}>
+
+ History
+
+ {history().map((item, i) => (
+ {item}
+ ))}
+
+ setHistory([])}>Clear History
+
+
+
+
+ Press Escape to clear history
+
+
+
+
+
+ )
+}
+
+const root = document.getElementById('root')!
+render(
+ () => (
+
+
+
+ ),
+ root,
+)
diff --git a/examples/solid/createHotkeySequences/tsconfig.json b/examples/solid/createHotkeySequences/tsconfig.json
new file mode 100644
index 00000000..cf560b81
--- /dev/null
+++ b/examples/solid/createHotkeySequences/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "Bundler",
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "strict": true,
+ "types": ["vite/client", "solid-js"]
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/examples/solid/createHotkeySequences/vite.config.ts b/examples/solid/createHotkeySequences/vite.config.ts
new file mode 100644
index 00000000..4095d9be
--- /dev/null
+++ b/examples/solid/createHotkeySequences/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import solid from 'vite-plugin-solid'
+
+export default defineConfig({
+ plugins: [solid()],
+})
diff --git a/examples/svelte/create-hotkey-sequence/src/App.svelte b/examples/svelte/create-hotkey-sequence/src/App.svelte
index f1bc7df1..0440ca13 100644
--- a/examples/svelte/create-hotkey-sequence/src/App.svelte
+++ b/examples/svelte/create-hotkey-sequence/src/App.svelte
@@ -7,6 +7,7 @@
let lastSequence = $state(null)
let history = $state>([])
+ let helloSequenceEnabled = $state(true)
function addToHistory(action: string) {
lastSequence = action
@@ -34,8 +35,10 @@
{ timeout: 1500 },
)
- createHotkeySequence(['H', 'E', 'L', 'L', 'O'], () =>
- addToHistory('hello → Hello World!'),
+ createHotkeySequence(
+ ['H', 'E', 'L', 'L', 'O'],
+ () => addToHistory('hello → Hello World!'),
+ () => ({ enabled: helloSequenceEnabled }),
)
createHotkeySequence(['Shift+R', 'Shift+T'], () =>
@@ -132,6 +135,16 @@
h e l l o
Type "hello" quickly
+
+ This sequence is
+ {helloSequenceEnabled ? 'enabled' : 'disabled'} .
+
+ (helloSequenceEnabled = !helloSequenceEnabled)}
+ >
+ {helloSequenceEnabled ? 'Disable' : 'Enable'} sequence
+
diff --git a/examples/svelte/create-hotkey-sequence/src/index.css b/examples/svelte/create-hotkey-sequence/src/index.css
index 8ee43876..0feff637 100644
--- a/examples/svelte/create-hotkey-sequence/src/index.css
+++ b/examples/svelte/create-hotkey-sequence/src/index.css
@@ -87,6 +87,13 @@ kbd {
.sequence-card p {
margin: 0 0 8px;
}
+.sequence-toggle-status {
+ font-size: 13px;
+ color: #555;
+ font-style: normal;
+ margin: 12px 0 10px !important;
+ line-height: 1.4;
+}
.hint {
font-size: 12px;
color: #888;
diff --git a/examples/svelte/create-hotkey-sequences/.gitignore b/examples/svelte/create-hotkey-sequences/.gitignore
new file mode 100644
index 00000000..3b462cb0
--- /dev/null
+++ b/examples/svelte/create-hotkey-sequences/.gitignore
@@ -0,0 +1,23 @@
+node_modules
+
+# Output
+.output
+.vercel
+.netlify
+.wrangler
+/.svelte-kit
+/build
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Env
+.env
+.env.*
+!.env.example
+!.env.test
+
+# Vite
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
diff --git a/examples/svelte/create-hotkey-sequences/.npmrc b/examples/svelte/create-hotkey-sequences/.npmrc
new file mode 100644
index 00000000..b6f27f13
--- /dev/null
+++ b/examples/svelte/create-hotkey-sequences/.npmrc
@@ -0,0 +1 @@
+engine-strict=true
diff --git a/examples/svelte/create-hotkey-sequences/README.md b/examples/svelte/create-hotkey-sequences/README.md
new file mode 100644
index 00000000..661b3b80
--- /dev/null
+++ b/examples/svelte/create-hotkey-sequences/README.md
@@ -0,0 +1,42 @@
+# sv
+
+Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
+
+## Creating a project
+
+If you're seeing this, you've probably already done this step. Congrats!
+
+```sh
+# create a new project
+npx sv create my-app
+```
+
+To recreate this project with the same configuration:
+
+```sh
+# recreate this project
+pnpm dlx sv create --template minimal --types ts --install pnpm create-hotkey-sequences
+```
+
+## Developing
+
+Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
+
+```sh
+npm run dev
+
+# or start the server and open the app in a new browser tab
+npm run dev -- --open
+```
+
+## Building
+
+To create a production version of your app:
+
+```sh
+npm run build
+```
+
+You can preview the production build with `npm run preview`.
+
+> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
diff --git a/examples/svelte/create-hotkey-sequences/index.html b/examples/svelte/create-hotkey-sequences/index.html
new file mode 100644
index 00000000..5ca121cc
--- /dev/null
+++ b/examples/svelte/create-hotkey-sequences/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ createHotkeySequences - TanStack Hotkeys Svelte Example
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/examples/svelte/create-hotkey-sequences/package.json b/examples/svelte/create-hotkey-sequences/package.json
new file mode 100644
index 00000000..e9641353
--- /dev/null
+++ b/examples/svelte/create-hotkey-sequences/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@tanstack/hotkeys-example-svelte-create-hotkey-sequences",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3069"
+ },
+ "dependencies": {
+ "@tanstack/svelte-hotkeys": "0.6.0",
+ "svelte": "^5.54.1"
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "^7.0.0",
+ "typescript": "5.9.3",
+ "vite": "^8.0.1"
+ }
+}
diff --git a/examples/svelte/create-hotkey-sequences/src/App.svelte b/examples/svelte/create-hotkey-sequences/src/App.svelte
new file mode 100644
index 00000000..5166b10b
--- /dev/null
+++ b/examples/svelte/create-hotkey-sequences/src/App.svelte
@@ -0,0 +1,245 @@
+
+
+
+
+
+
+
+ Vim-Style Commands
+
+
+
+ Sequence
+ Action
+
+
+
+
+
+ g g
+
+ Go to top
+
+
+
+ G (Shift+G)
+
+ Go to bottom
+
+
+
+ d d
+
+ Delete line
+
+
+
+ y y
+
+ Yank (copy) line
+
+
+
+ d w
+
+ Delete word
+
+
+
+ c i w
+
+ Change inner word
+
+
+
+
+
+
+ Fun Sequences
+
+
+
Konami Code (Partial)
+
+ ↑ ↑ ↓ ↓
+
+
Use arrow keys within 1.5 seconds
+
+
+
Side to Side
+
+ ← → ← →
+
+
Arrow keys within 1.5 seconds
+
+
+
Spell It Out
+
+ h e l l o
+
+
Type "hello" quickly
+
+ This sequence is
+ {helloSequenceEnabled ? 'enabled' : 'disabled'} .
+
+
(helloSequenceEnabled = !helloSequenceEnabled)}
+ >
+ {helloSequenceEnabled ? 'Disable' : 'Enable'} sequence
+
+
+
+
+
+ {#if lastSequence}
+
+ Triggered:
+ {lastSequence}
+
+ {/if}
+
+
+
+
+ Chained Shift+letter sequences
+
+ Each step is a chord: hold Shift and press a letter. You can
+ press Shift alone between steps—those modifier-only presses do
+ not reset progress, so the next chord still counts.
+
+
+
+
+ Sequence
+ Action
+
+
+
+
+
+ Shift +r then Shift +t
+
+ Chained Shift+letter (2 steps)
+
+
+
+
+
+
+ Usage
+ {`import { createHotkeySequences } from '@tanstack/svelte-hotkeys'
+
+`}
+
+
+ {#if history.length > 0}
+
+ History
+
+ {#each history as item}
+ {item}
+ {/each}
+
+ (history = [])}>Clear History
+
+ {/if}
+
+
+ Press Escape to clear history
+
+
+
diff --git a/examples/svelte/create-hotkey-sequences/src/Root.svelte b/examples/svelte/create-hotkey-sequences/src/Root.svelte
new file mode 100644
index 00000000..12e94e98
--- /dev/null
+++ b/examples/svelte/create-hotkey-sequences/src/Root.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/examples/svelte/create-hotkey-sequences/src/index.css b/examples/svelte/create-hotkey-sequences/src/index.css
new file mode 100644
index 00000000..cfa1d453
--- /dev/null
+++ b/examples/svelte/create-hotkey-sequences/src/index.css
@@ -0,0 +1,168 @@
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+ font-family:
+ system-ui,
+ -apple-system,
+ sans-serif;
+ background: #f5f5f5;
+ color: #333;
+}
+.app {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+}
+header {
+ text-align: center;
+ margin-bottom: 40px;
+}
+header h1 {
+ margin: 0 0 10px;
+ color: #0066cc;
+}
+header p {
+ color: #666;
+ max-width: 500px;
+ margin: 0 auto;
+}
+
+.demo-section {
+ background: white;
+ border-radius: 12px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+.demo-section h2 {
+ margin: 0 0 16px;
+ font-size: 20px;
+}
+.demo-section p {
+ margin: 0 0 12px;
+}
+kbd {
+ background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%);
+ border: 1px solid #ccc;
+ border-bottom-width: 2px;
+ border-radius: 4px;
+ padding: 2px 8px;
+ font-family: monospace;
+ font-size: 13px;
+ margin-right: 4px;
+}
+.sequence-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+.sequence-table th,
+.sequence-table td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid #eee;
+}
+.sequence-table th {
+ font-weight: 600;
+ color: #666;
+ font-size: 14px;
+}
+.fun-sequences {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+}
+.sequence-card {
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 16px;
+ text-align: center;
+}
+.sequence-card h3 {
+ margin: 0 0 12px;
+ font-size: 16px;
+}
+.sequence-card p {
+ margin: 0 0 8px;
+}
+.sequence-toggle-status {
+ font-size: 13px;
+ color: #555;
+ font-style: normal;
+ margin: 12px 0 10px !important;
+ line-height: 1.4;
+}
+.hint {
+ font-size: 12px;
+ color: #888;
+ font-style: italic;
+}
+.info-box {
+ background: #e3f2fd;
+ border-radius: 8px;
+ padding: 16px 20px;
+ margin-bottom: 24px;
+ font-size: 18px;
+}
+.info-box.success {
+ background: #e8f5e9;
+ color: #2e7d32;
+}
+.code-block {
+ background: #1e1e1e;
+ color: #d4d4d4;
+ padding: 16px;
+ border-radius: 8px;
+ overflow-x: auto;
+ font-size: 13px;
+ line-height: 1.5;
+}
+.history-list {
+ list-style: none;
+ padding: 0;
+ margin: 0 0 16px;
+}
+.history-list li {
+ padding: 10px 14px;
+ background: #f0f0f0;
+ border-radius: 6px;
+ margin-bottom: 6px;
+ font-family: monospace;
+ font-size: 14px;
+}
+button {
+ background: #0066cc;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+}
+button:hover {
+ background: #0052a3;
+}
+
+.counter {
+ font-size: 18px;
+ font-weight: bold;
+ color: #0066cc;
+ margin: 12px 0;
+}
+
+.demo-input {
+ width: 100%;
+ max-width: 400px;
+ padding: 12px 16px;
+ font-size: 14px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ margin-top: 8px;
+}
+
+.demo-input:focus {
+ outline: 2px solid #0066cc;
+ outline-offset: 2px;
+ border-color: #0066cc;
+}
diff --git a/examples/svelte/create-hotkey-sequences/src/main.ts b/examples/svelte/create-hotkey-sequences/src/main.ts
new file mode 100644
index 00000000..93579001
--- /dev/null
+++ b/examples/svelte/create-hotkey-sequences/src/main.ts
@@ -0,0 +1,5 @@
+import { mount } from 'svelte'
+import Root from './Root.svelte'
+import './index.css'
+
+mount(Root, { target: document.getElementById('app')! })
diff --git a/examples/svelte/create-hotkey-sequences/static/robots.txt b/examples/svelte/create-hotkey-sequences/static/robots.txt
new file mode 100644
index 00000000..b6dd6670
--- /dev/null
+++ b/examples/svelte/create-hotkey-sequences/static/robots.txt
@@ -0,0 +1,3 @@
+# allow crawling everything by default
+User-agent: *
+Disallow:
diff --git a/examples/svelte/create-hotkey-sequences/svelte.config.js b/examples/svelte/create-hotkey-sequences/svelte.config.js
new file mode 100644
index 00000000..b30b6572
--- /dev/null
+++ b/examples/svelte/create-hotkey-sequences/svelte.config.js
@@ -0,0 +1,11 @@
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
+
+/** @type {import('svelte').Config} */
+const config = {
+ preprocess: vitePreprocess(),
+ compilerOptions: {
+ runes: true,
+ },
+}
+
+export default config
diff --git a/examples/svelte/create-hotkey-sequences/tsconfig.json b/examples/svelte/create-hotkey-sequences/tsconfig.json
new file mode 100644
index 00000000..912a0303
--- /dev/null
+++ b/examples/svelte/create-hotkey-sequences/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "moduleResolution": "bundler"
+ },
+ "include": ["src"],
+ "exclude": ["eslint.config.js"]
+}
diff --git a/examples/svelte/create-hotkey-sequences/vite.config.ts b/examples/svelte/create-hotkey-sequences/vite.config.ts
new file mode 100644
index 00000000..951a9ba4
--- /dev/null
+++ b/examples/svelte/create-hotkey-sequences/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import { svelte } from '@sveltejs/vite-plugin-svelte'
+
+export default defineConfig({
+ plugins: [svelte()],
+})
diff --git a/examples/vue/useHotkeySequence/src/App.vue b/examples/vue/useHotkeySequence/src/App.vue
index 57c1867d..3291fc3b 100644
--- a/examples/vue/useHotkeySequence/src/App.vue
+++ b/examples/vue/useHotkeySequence/src/App.vue
@@ -10,6 +10,7 @@ import { ref } from 'vue'
const lastSequence = ref(null)
const history = ref>([])
+const helloSequenceEnabled = ref(true)
const plugins = [{ name: 'TanStack Hotkeys', component: HotkeysDevtoolsPanel }]
const addToHistory = (action: string) => {
@@ -38,8 +39,10 @@ useHotkeySequence(
{ timeout: 1500 },
)
-useHotkeySequence(['H', 'E', 'L', 'L', 'O'], () =>
- addToHistory('hello → Hello World!'),
+useHotkeySequence(
+ ['H', 'E', 'L', 'L', 'O'],
+ () => addToHistory('hello → Hello World!'),
+ () => ({ enabled: helloSequenceEnabled.value }),
)
useHotkeySequence(['Shift+R', 'Shift+T'], () =>
@@ -147,6 +150,19 @@ function VimEditor() {
h e l l o
Type "hello" quickly
+
+ This sequence is
+ {{
+ helloSequenceEnabled ? 'enabled' : 'disabled'
+ }} .
+
+
+ {{ helloSequenceEnabled ? 'Disable' : 'Enable' }} sequence
+
diff --git a/examples/vue/useHotkeySequence/src/index.css b/examples/vue/useHotkeySequence/src/index.css
index 69b749be..e3437f77 100644
--- a/examples/vue/useHotkeySequence/src/index.css
+++ b/examples/vue/useHotkeySequence/src/index.css
@@ -84,6 +84,13 @@ kbd {
.sequence-card p {
margin: 0 0 8px;
}
+.sequence-toggle-status {
+ font-size: 13px;
+ color: #555;
+ font-style: normal;
+ margin: 12px 0 10px !important;
+ line-height: 1.4;
+}
.hint {
font-size: 12px;
color: #888;
diff --git a/examples/vue/useHotkeySequences/eslint.config.js b/examples/vue/useHotkeySequences/eslint.config.js
new file mode 100644
index 00000000..a47f4a62
--- /dev/null
+++ b/examples/vue/useHotkeySequences/eslint.config.js
@@ -0,0 +1,11 @@
+// @ts-check
+
+import rootConfig from '../../../eslint.config.js'
+
+/** @type {import('eslint').Linter.Config[]} */
+export default [
+ {
+ ignores: ['eslint.config.js', 'vite.config.ts'],
+ },
+ ...rootConfig,
+]
diff --git a/examples/vue/useHotkeySequences/index.html b/examples/vue/useHotkeySequences/index.html
new file mode 100644
index 00000000..8d533559
--- /dev/null
+++ b/examples/vue/useHotkeySequences/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ useHotkeySequences - TanStack Hotkeys Vue Example
+
+
+
+
+
+
diff --git a/examples/vue/useHotkeySequences/package.json b/examples/vue/useHotkeySequences/package.json
new file mode 100644
index 00000000..95ab61fd
--- /dev/null
+++ b/examples/vue/useHotkeySequences/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@tanstack/hotkeys-example-vue-use-hotkey-sequences",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3069",
+ "build": "vite build",
+ "preview": "vite preview",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "test:types": "tsc"
+ },
+ "dependencies": {
+ "@tanstack/vue-hotkeys": "^0.6.0",
+ "vue": "^3.5.30"
+ },
+ "devDependencies": {
+ "@tanstack/vue-devtools": "^0.2.14",
+ "@tanstack/vue-hotkeys-devtools": "^0.5.0",
+ "@vitejs/plugin-vue": "^6.0.5",
+ "typescript": "5.9.3",
+ "vite": "^8.0.1"
+ }
+}
diff --git a/examples/vue/useHotkeySequences/src/App.vue b/examples/vue/useHotkeySequences/src/App.vue
new file mode 100644
index 00000000..c33e4548
--- /dev/null
+++ b/examples/vue/useHotkeySequences/src/App.vue
@@ -0,0 +1,232 @@
+
+
+
+
+
+
+
+
+
+ Vim-Style Commands
+
+
+
+ Sequence
+ Action
+
+
+
+
+ g g
+ Go to top
+
+
+ G (Shift+G)
+ Go to bottom
+
+
+ d d
+ Delete line
+
+
+ y y
+ Yank (copy) line
+
+
+ d w
+ Delete word
+
+
+ c i w
+ Change inner word
+
+
+
+
+
+
+ Fun Sequences
+
+
+
Konami Code (Partial)
+
↑ ↑ ↓ ↓
+
Use arrow keys within 1.5 seconds
+
+
+
Side to Side
+
← → ← →
+
Arrow keys within 1.5 seconds
+
+
+
Spell It Out
+
+ h e l l o
+
+
Type "hello" quickly
+
+ This sequence is
+ {{
+ helloSequenceEnabled ? 'enabled' : 'disabled'
+ }} .
+
+
+ {{ helloSequenceEnabled ? 'Disable' : 'Enable' }} sequence
+
+
+
+
+
+
+ Triggered: {{ lastSequence }}
+
+
+
+
+
+ Chained Shift+letter sequences
+
+ Each step is a chord: hold Shift and press a letter. You
+ can press Shift alone between steps—those modifier-only
+ presses do not reset progress, so the next chord still counts.
+
+
+
+
+ Sequence
+ Action
+
+
+
+
+
+ Shift +r then Shift +
+ t
+
+ Chained Shift+letter (2 steps)
+
+
+
+
+
+
+ Usage
+ {{ usageCode }}
+
+
+
+ History
+
+ Clear History
+
+
+ Press Escape to clear history
+
+
+
+
+
+
diff --git a/examples/vue/useHotkeySequences/src/index.css b/examples/vue/useHotkeySequences/src/index.css
new file mode 100644
index 00000000..d051158b
--- /dev/null
+++ b/examples/vue/useHotkeySequences/src/index.css
@@ -0,0 +1,165 @@
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+ font-family:
+ system-ui,
+ -apple-system,
+ sans-serif;
+ background: #f5f5f5;
+ color: #333;
+}
+.app {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+}
+header {
+ text-align: center;
+ margin-bottom: 40px;
+}
+header h1 {
+ margin: 0 0 10px;
+ color: #0066cc;
+}
+header p {
+ color: #666;
+ max-width: 500px;
+ margin: 0 auto;
+}
+
+.demo-section {
+ background: white;
+ border-radius: 12px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+.demo-section h2 {
+ margin: 0 0 16px;
+ font-size: 20px;
+}
+kbd {
+ background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%);
+ border: 1px solid #ccc;
+ border-bottom-width: 2px;
+ border-radius: 4px;
+ padding: 2px 8px;
+ font-family: monospace;
+ font-size: 13px;
+ margin-right: 4px;
+}
+.sequence-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+.sequence-table th,
+.sequence-table td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid #eee;
+}
+.sequence-table th {
+ font-weight: 600;
+ color: #666;
+ font-size: 14px;
+}
+.fun-sequences {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+}
+.sequence-card {
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 16px;
+ text-align: center;
+}
+.sequence-card h3 {
+ margin: 0 0 12px;
+ font-size: 16px;
+}
+.sequence-card p {
+ margin: 0 0 8px;
+}
+.sequence-toggle-status {
+ font-size: 13px;
+ color: #555;
+ font-style: normal;
+ margin: 12px 0 10px !important;
+ line-height: 1.4;
+}
+.hint {
+ font-size: 12px;
+ color: #888;
+ font-style: italic;
+}
+.info-box {
+ background: #e3f2fd;
+ border-radius: 8px;
+ padding: 16px 20px;
+ margin-bottom: 24px;
+ font-size: 18px;
+}
+.info-box.success {
+ background: #e8f5e9;
+ color: #2e7d32;
+}
+.code-block {
+ background: #1e1e1e;
+ color: #d4d4d4;
+ padding: 16px;
+ border-radius: 8px;
+ overflow-x: auto;
+ font-size: 13px;
+ line-height: 1.5;
+}
+.history-list {
+ list-style: none;
+ padding: 0;
+ margin: 0 0 16px;
+}
+.history-list li {
+ padding: 10px 14px;
+ background: #f0f0f0;
+ border-radius: 6px;
+ margin-bottom: 6px;
+ font-family: monospace;
+ font-size: 14px;
+}
+button {
+ background: #0066cc;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+}
+button:hover {
+ background: #0052a3;
+}
+
+.counter {
+ font-size: 18px;
+ font-weight: bold;
+ color: #0066cc;
+ margin: 12px 0;
+}
+
+.demo-input {
+ width: 100%;
+ max-width: 400px;
+ padding: 12px 16px;
+ font-size: 14px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ margin-top: 8px;
+}
+
+.demo-input:focus {
+ outline: 2px solid #0066cc;
+ outline-offset: 2px;
+ border-color: #0066cc;
+}
diff --git a/examples/vue/useHotkeySequences/src/index.ts b/examples/vue/useHotkeySequences/src/index.ts
new file mode 100644
index 00000000..50a4dab0
--- /dev/null
+++ b/examples/vue/useHotkeySequences/src/index.ts
@@ -0,0 +1,5 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import './index.css'
+
+createApp(App).mount('#app')
diff --git a/examples/vue/useHotkeySequences/src/vue.d.ts b/examples/vue/useHotkeySequences/src/vue.d.ts
new file mode 100644
index 00000000..b07a0596
--- /dev/null
+++ b/examples/vue/useHotkeySequences/src/vue.d.ts
@@ -0,0 +1,6 @@
+declare module '*.vue' {
+ import type { DefineComponent } from 'vue'
+
+ const component: DefineComponent<{}, {}, any>
+ export default component
+}
diff --git a/examples/vue/useHotkeySequences/tsconfig.json b/examples/vue/useHotkeySequences/tsconfig.json
new file mode 100644
index 00000000..b1d72611
--- /dev/null
+++ b/examples/vue/useHotkeySequences/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve"
+ },
+ "include": ["src"],
+ "exclude": ["eslint.config.js"]
+}
diff --git a/examples/vue/useHotkeySequences/vite.config.ts b/examples/vue/useHotkeySequences/vite.config.ts
new file mode 100644
index 00000000..c40aa3c3
--- /dev/null
+++ b/examples/vue/useHotkeySequences/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+ plugins: [vue()],
+})
diff --git a/packages/angular-hotkeys/src/index.ts b/packages/angular-hotkeys/src/index.ts
index 0101fd77..d19de42e 100644
--- a/packages/angular-hotkeys/src/index.ts
+++ b/packages/angular-hotkeys/src/index.ts
@@ -8,6 +8,7 @@ export * from './hotkeys-provider'
export * from './injectHotkey'
export * from './injectHotkeys'
export * from './injectHotkeySequence'
+export * from './injectHotkeySequences'
export * from './injectHeldKeys'
export * from './injectHeldKeyCodes'
export * from './injectKeyHold'
diff --git a/packages/angular-hotkeys/src/injectHotkey.ts b/packages/angular-hotkeys/src/injectHotkey.ts
index 28654b70..fac63e5e 100644
--- a/packages/angular-hotkeys/src/injectHotkey.ts
+++ b/packages/angular-hotkeys/src/injectHotkey.ts
@@ -1,4 +1,4 @@
-import { effect } from '@angular/core'
+import { DestroyRef, effect, inject } from '@angular/core'
import {
detectPlatform,
formatHotkey,
@@ -10,6 +10,7 @@ import type {
Hotkey,
HotkeyCallback,
HotkeyOptions,
+ HotkeyRegistrationHandle,
RegisterableHotkey,
} from '@tanstack/hotkeys'
@@ -34,6 +35,8 @@ export interface InjectHotkeyOptions extends Omit {
* Call in an injection context (e.g. constructor or field initializer).
* Uses effect() to track reactive dependencies and update registration
* when options or the callback change.
+ * `enabled: false` keeps the registration (visible in devtools) and only suppresses firing; the same
+ * handle is updated instead of unregistering and re-registering when identity is unchanged.
*
* @param hotkey - The hotkey string (e.g. 'Mod+S', 'Escape') or getter function
* @param callback - The function to call when the hotkey is pressed
@@ -87,8 +90,22 @@ export function injectHotkey(
): void {
const defaultOptions = injectDefaultHotkeysOptions()
const manager = getHotkeyManager()
+ const destroyRef = inject(DestroyRef)
- effect((onCleanup) => {
+ let registration: HotkeyRegistrationHandle | null = null
+ let lastHotkeyString: Hotkey | null = null
+ let lastTarget: HTMLElement | Document | Window | null = null
+
+ destroyRef.onDestroy(() => {
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
+ }
+ lastHotkeyString = null
+ lastTarget = null
+ })
+
+ effect(() => {
// Resolve reactive values
const resolvedHotkey = typeof hotkey === 'function' ? hotkey() : hotkey
const resolvedOptions = typeof options === 'function' ? options() : options
@@ -117,29 +134,44 @@ export function injectHotkey(
: null
if (!resolvedTarget) {
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
+ }
+ lastHotkeyString = null
+ lastTarget = null
return
}
// Extract options without target (target is handled separately)
const { target: _target, ...optionsWithoutTarget } = mergedOptions
- // Register the hotkey
- const registration = manager.register(hotkeyString, callback, {
+ if (
+ registration?.isActive &&
+ lastHotkeyString === hotkeyString &&
+ lastTarget === resolvedTarget
+ ) {
+ registration.callback = callback
+ registration.setOptions(optionsWithoutTarget)
+ return
+ }
+
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
+ }
+
+ registration = manager.register(hotkeyString, callback, {
...optionsWithoutTarget,
target: resolvedTarget,
})
- // Update callback and options on every effect run
if (registration.isActive) {
registration.callback = callback
registration.setOptions(optionsWithoutTarget)
}
- // Cleanup on disposal
- onCleanup(() => {
- if (registration.isActive) {
- registration.unregister()
- }
- })
+ lastHotkeyString = hotkeyString
+ lastTarget = resolvedTarget
})
}
diff --git a/packages/angular-hotkeys/src/injectHotkeySequence.ts b/packages/angular-hotkeys/src/injectHotkeySequence.ts
index 99d1069c..d7e7a146 100644
--- a/packages/angular-hotkeys/src/injectHotkeySequence.ts
+++ b/packages/angular-hotkeys/src/injectHotkeySequence.ts
@@ -1,18 +1,27 @@
-import { effect } from '@angular/core'
-import { getSequenceManager } from '@tanstack/hotkeys'
+import { DestroyRef, effect, inject } from '@angular/core'
+import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
import { injectDefaultHotkeysOptions } from './hotkeys-provider'
import type {
HotkeyCallback,
HotkeySequence,
SequenceOptions,
+ SequenceRegistrationHandle,
} from '@tanstack/hotkeys'
+type SequenceTarget = Document | HTMLElement | Window
+
export interface InjectHotkeySequenceOptions extends Omit<
SequenceOptions,
- 'enabled'
+ 'enabled' | 'target'
> {
/** Whether the sequence is enabled. Defaults to true. */
enabled?: boolean
+ /**
+ * The DOM element to attach the event listener to.
+ * Can be a direct DOM element, an accessor target, or null.
+ * Defaults to document when omitted.
+ */
+ target?: HTMLElement | Document | Window | null
}
/**
@@ -28,7 +37,8 @@ export interface InjectHotkeySequenceOptions extends Omit<
*
* @param sequence - Array of hotkey strings that form the sequence (or getter function)
* @param callback - Function to call when the sequence is completed
- * @param options - Options for the sequence behavior (or getter function)
+ * @param options - Options for the sequence behavior (or getter function). `enabled: false` still registers
+ * the sequence (visible in devtools); only execution is suppressed.
*
* @example
* ```ts
@@ -53,8 +63,23 @@ export function injectHotkeySequence(
| (() => InjectHotkeySequenceOptions) = {},
): void {
const defaultOptions = injectDefaultHotkeysOptions()
+ const manager = getSequenceManager()
+ const destroyRef = inject(DestroyRef)
+
+ let handle: SequenceRegistrationHandle | null = null
+ let lastSequenceKey: string | null = null
+ let lastTarget: SequenceTarget | null = null
- effect((onCleanup) => {
+ destroyRef.onDestroy(() => {
+ if (handle?.isActive) {
+ handle.unregister()
+ handle = null
+ }
+ lastSequenceKey = null
+ lastTarget = null
+ })
+
+ effect(() => {
// Resolve reactive values
const resolvedSequence =
typeof sequence === 'function' ? sequence() : sequence
@@ -67,27 +92,49 @@ export function injectHotkeySequence(
const { enabled = true, ...sequenceOptions } = mergedOptions
- if (!enabled || resolvedSequence.length === 0) {
+ const resolvedTarget =
+ 'target' in sequenceOptions
+ ? (sequenceOptions.target ?? null)
+ : typeof document !== 'undefined'
+ ? document
+ : null
+
+ if (resolvedSequence.length === 0 || !resolvedTarget) {
+ if (handle?.isActive) {
+ handle.unregister()
+ handle = null
+ }
+ lastSequenceKey = null
+ lastTarget = null
return
}
- const manager = getSequenceManager()
+ const sequenceKey = formatHotkeySequence(resolvedSequence)
- // Pass through options; default target to document when not provided
- const registerOptions: SequenceOptions = {
+ const registerPayload: SequenceOptions = {
...sequenceOptions,
- enabled: true,
- target:
- sequenceOptions.target ??
- (typeof document !== 'undefined' ? document : undefined),
+ enabled,
+ target: resolvedTarget,
}
+ const { target: _t, ...optionsWithoutTarget } = registerPayload
- const handle = manager.register(resolvedSequence, callback, registerOptions)
+ if (
+ handle?.isActive &&
+ lastSequenceKey === sequenceKey &&
+ lastTarget === resolvedTarget
+ ) {
+ handle.callback = callback
+ handle.setOptions(optionsWithoutTarget)
+ return
+ }
- onCleanup(() => {
- if (handle.isActive) {
- handle.unregister()
- }
- })
+ if (handle?.isActive) {
+ handle.unregister()
+ handle = null
+ }
+
+ handle = manager.register(resolvedSequence, callback, registerPayload)
+ lastSequenceKey = sequenceKey
+ lastTarget = resolvedTarget
})
}
diff --git a/packages/angular-hotkeys/src/injectHotkeySequences.ts b/packages/angular-hotkeys/src/injectHotkeySequences.ts
new file mode 100644
index 00000000..102ae9b3
--- /dev/null
+++ b/packages/angular-hotkeys/src/injectHotkeySequences.ts
@@ -0,0 +1,176 @@
+import { DestroyRef, effect, inject } from '@angular/core'
+import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
+import { injectDefaultHotkeysOptions } from './hotkeys-provider'
+import type { InjectHotkeySequenceOptions } from './injectHotkeySequence'
+import type {
+ HotkeyCallback,
+ HotkeySequence,
+ SequenceRegistrationHandle,
+} from '@tanstack/hotkeys'
+
+/**
+ * A single sequence definition for use with `injectHotkeySequences`.
+ */
+export interface InjectHotkeySequenceDefinition {
+ /** Array of hotkey strings that form the sequence */
+ sequence: HotkeySequence | (() => HotkeySequence)
+ /** The function to call when the sequence is completed */
+ callback: HotkeyCallback
+ /** Per-sequence options (merged on top of commonOptions) */
+ options?: InjectHotkeySequenceOptions | (() => InjectHotkeySequenceOptions)
+}
+
+/**
+ * Angular inject-based API for registering multiple keyboard shortcut sequences at once (Vim-style).
+ *
+ * Uses the singleton SequenceManager. Call in an injection context (e.g. constructor).
+ * Uses `effect()` to track reactive dependencies when definitions or options are getters.
+ *
+ * Options are merged in this order:
+ * provideHotkeys defaults < commonOptions < per-definition options
+ *
+ * Definitions with an empty `sequence` are skipped. Disabled sequences (`enabled: false`)
+ * remain registered so they stay visible in devtools; the core manager suppresses execution.
+ *
+ * @param sequences - Array of sequence definitions, or getter returning them
+ * @param commonOptions - Shared options for all sequences, or getter
+ *
+ * @example
+ * ```ts
+ * @Component({ ... })
+ * export class VimComponent {
+ * constructor() {
+ * injectHotkeySequences([
+ * { sequence: ['G', 'G'], callback: () => this.goTop() },
+ * { sequence: ['D', 'D'], callback: () => this.deleteLine() },
+ * ])
+ * }
+ * }
+ * ```
+ */
+export function injectHotkeySequences(
+ sequences:
+ | Array
+ | (() => Array),
+ commonOptions:
+ | InjectHotkeySequenceOptions
+ | (() => InjectHotkeySequenceOptions) = {},
+): void {
+ type RegistrationRecord = {
+ handle: SequenceRegistrationHandle
+ target: Document | HTMLElement | Window
+ }
+
+ const defaultOptions = injectDefaultHotkeysOptions()
+ const manager = getSequenceManager()
+ const destroyRef = inject(DestroyRef)
+
+ const registrations = new Map()
+
+ destroyRef.onDestroy(() => {
+ for (const { handle } of registrations.values()) {
+ if (handle.isActive) {
+ handle.unregister()
+ }
+ }
+ registrations.clear()
+ })
+
+ effect(() => {
+ const resolvedSequences =
+ typeof sequences === 'function' ? sequences() : sequences
+ const resolvedCommonOptions =
+ typeof commonOptions === 'function' ? commonOptions() : commonOptions
+
+ const nextKeys = new Set()
+ const prepared: Array<{
+ registrationKey: string
+ def: InjectHotkeySequenceDefinition
+ resolvedSequence: HotkeySequence
+ enabled: boolean
+ sequenceOpts: Omit
+ resolvedTarget: Document | HTMLElement | Window
+ }> = []
+
+ for (let i = 0; i < resolvedSequences.length; i++) {
+ const def = resolvedSequences[i]!
+ const resolvedSequence =
+ typeof def.sequence === 'function' ? def.sequence() : def.sequence
+ const resolvedDefOptions =
+ typeof def.options === 'function' ? def.options() : (def.options ?? {})
+
+ const mergedOptions = {
+ ...defaultOptions.hotkeySequence,
+ ...resolvedCommonOptions,
+ ...resolvedDefOptions,
+ } as InjectHotkeySequenceOptions
+
+ const { enabled = true, ...sequenceOpts } = mergedOptions
+
+ if (resolvedSequence.length === 0) {
+ continue
+ }
+
+ const resolvedTarget =
+ 'target' in sequenceOpts
+ ? (sequenceOpts.target ?? null)
+ : typeof document !== 'undefined'
+ ? document
+ : null
+
+ if (!resolvedTarget) {
+ continue
+ }
+
+ const registrationKey = `${i}:${formatHotkeySequence(resolvedSequence)}`
+ nextKeys.add(registrationKey)
+ prepared.push({
+ registrationKey,
+ def,
+ resolvedSequence,
+ enabled,
+ sequenceOpts,
+ resolvedTarget,
+ })
+ }
+
+ for (const [key, record] of [...registrations.entries()]) {
+ if (!nextKeys.has(key)) {
+ if (record.handle.isActive) {
+ record.handle.unregister()
+ }
+ registrations.delete(key)
+ }
+ }
+
+ for (const p of prepared) {
+ const existing = registrations.get(p.registrationKey)
+ if (existing?.handle.isActive && existing.target === p.resolvedTarget) {
+ existing.handle.callback = p.def.callback
+ const { target: _target, ...optionsWithoutTarget } = p.sequenceOpts
+ existing.handle.setOptions({
+ ...optionsWithoutTarget,
+ enabled: p.enabled,
+ })
+ continue
+ }
+
+ if (existing) {
+ if (existing.handle.isActive) {
+ existing.handle.unregister()
+ }
+ registrations.delete(p.registrationKey)
+ }
+
+ const handle = manager.register(p.resolvedSequence, p.def.callback, {
+ ...p.sequenceOpts,
+ enabled: p.enabled,
+ target: p.resolvedTarget,
+ })
+ registrations.set(p.registrationKey, {
+ handle,
+ target: p.resolvedTarget,
+ })
+ }
+ })
+}
diff --git a/packages/hotkeys/src/hotkey-manager.ts b/packages/hotkeys/src/hotkey-manager.ts
index bf9e685b..0714d45c 100644
--- a/packages/hotkeys/src/hotkey-manager.ts
+++ b/packages/hotkeys/src/hotkey-manager.ts
@@ -27,7 +27,11 @@ export type { ConflictBehavior }
export interface HotkeyOptions {
/** Behavior when this hotkey conflicts with an existing registration on the same target. Defaults to 'warn' */
conflictBehavior?: ConflictBehavior
- /** Whether the hotkey is enabled. Defaults to true */
+ /**
+ * Soft-disable: when `false`, the callback does not run but the registration
+ * stays in `HotkeyManager` (and in devtools). Toggling this should update the
+ * existing handle via `setOptions` rather than unregistering. Defaults to `true`.
+ */
enabled?: boolean
/** The event type to listen for. Defaults to 'keydown' */
eventType?: 'keydown' | 'keyup'
diff --git a/packages/hotkeys/tests/sequence-manager.test.ts b/packages/hotkeys/tests/sequence-manager.test.ts
index df17b3e0..30058b74 100644
--- a/packages/hotkeys/tests/sequence-manager.test.ts
+++ b/packages/hotkeys/tests/sequence-manager.test.ts
@@ -86,6 +86,27 @@ describe('SequenceManager', () => {
expect(manager.getRegistrationCount()).toBe(0)
})
+ it('should keep registration when disabled and skip callback until re-enabled', () => {
+ const manager = SequenceManager.getInstance()
+ const callback = vi.fn()
+
+ const handle = manager.register(['G', 'G'], callback, { enabled: false })
+
+ expect(manager.getRegistrationCount()).toBe(1)
+ expect(
+ [...manager.registrations.state.values()][0]?.options.enabled,
+ ).toBe(false)
+
+ dispatchKey('g')
+ dispatchKey('g')
+ expect(callback).not.toHaveBeenCalled()
+
+ handle.setOptions({ enabled: true })
+ dispatchKey('g')
+ dispatchKey('g')
+ expect(callback).toHaveBeenCalledTimes(1)
+ })
+
it('should return handle with callback and setOptions', () => {
const manager = SequenceManager.getInstance()
const callback1 = vi.fn()
diff --git a/packages/preact-hotkeys/src/index.ts b/packages/preact-hotkeys/src/index.ts
index c4463a2a..f144170e 100644
--- a/packages/preact-hotkeys/src/index.ts
+++ b/packages/preact-hotkeys/src/index.ts
@@ -11,5 +11,6 @@ export * from './useHeldKeys'
export * from './useHeldKeyCodes'
export * from './useKeyHold'
export * from './useHotkeySequence'
+export * from './useHotkeySequences'
export * from './useHotkeyRecorder'
export * from './useHotkeySequenceRecorder'
diff --git a/packages/preact-hotkeys/src/useHotkey.ts b/packages/preact-hotkeys/src/useHotkey.ts
index fd97eae2..012eaded 100644
--- a/packages/preact-hotkeys/src/useHotkey.ts
+++ b/packages/preact-hotkeys/src/useHotkey.ts
@@ -44,7 +44,8 @@ export interface UseHotkeyOptions extends Omit {
*
* @param hotkey - The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object (supports `mod` for cross-platform)
* @param callback - The function to call when the hotkey is pressed
- * @param options - Options for the hotkey behavior
+ * @param options - Options for the hotkey behavior. `enabled: false` keeps the registration (visible in devtools)
+ * and only suppresses firing; the hook updates the existing handle instead of unregistering.
*
* @example
* ```tsx
@@ -136,6 +137,12 @@ export function useHotkey(
// Skip if no valid target (SSR or ref still null)
if (!resolvedTarget) {
+ if (registrationRef.current?.isActive) {
+ registrationRef.current.unregister()
+ registrationRef.current = null
+ }
+ prevTargetRef.current = null
+ prevHotkeyRef.current = null
return
}
@@ -175,7 +182,7 @@ export function useHotkey(
registrationRef.current = null
}
}
- }, [hotkeyString, options.enabled])
+ }, [hotkeyString])
// Sync callback and options on EVERY render (outside useEffect)
// This avoids stale closures - the callback always has access to latest state
diff --git a/packages/preact-hotkeys/src/useHotkeySequence.ts b/packages/preact-hotkeys/src/useHotkeySequence.ts
index fce0fcf1..c2ae418b 100644
--- a/packages/preact-hotkeys/src/useHotkeySequence.ts
+++ b/packages/preact-hotkeys/src/useHotkeySequence.ts
@@ -41,7 +41,8 @@ export interface UseHotkeySequenceOptions extends Omit<
*
* @param sequence - Array of hotkey strings that form the sequence
* @param callback - Function to call when the sequence is completed
- * @param options - Options for the sequence behavior
+ * @param options - Options for the sequence behavior. `enabled: false` keeps the registration (visible in devtools)
+ * and only suppresses firing; the hook updates the existing handle instead of unregistering.
*
* @example
* ```tsx
@@ -89,11 +90,13 @@ export function useHotkeySequence(
const callbackRef = useRef(callback)
const optionsRef = useRef(mergedOptions)
const managerRef = useRef(manager)
+ const sequenceRef = useRef(sequence)
// Update refs on every render
callbackRef.current = callback
optionsRef.current = mergedOptions
managerRef.current = manager
+ sequenceRef.current = sequence
// Track previous target and sequence to detect changes requiring re-registration
const prevTargetRef = useRef(null)
@@ -106,7 +109,13 @@ export function useHotkeySequence(
const { target: _target, ...optionsWithoutTarget } = mergedOptions
useEffect(() => {
- if (sequence.length === 0) {
+ if (sequenceRef.current.length === 0) {
+ if (registrationRef.current?.isActive) {
+ registrationRef.current.unregister()
+ registrationRef.current = null
+ }
+ prevTargetRef.current = null
+ prevSequenceRef.current = null
return
}
@@ -118,6 +127,12 @@ export function useHotkeySequence(
// Skip if no valid target (SSR or ref still null)
if (!resolvedTarget) {
+ if (registrationRef.current?.isActive) {
+ registrationRef.current.unregister()
+ registrationRef.current = null
+ }
+ prevTargetRef.current = null
+ prevSequenceRef.current = null
return
}
@@ -140,7 +155,7 @@ export function useHotkeySequence(
// Register if needed (no active registration)
if (!registrationRef.current || !registrationRef.current.isActive) {
registrationRef.current = managerRef.current.register(
- sequence,
+ sequenceRef.current,
(event, context) => callbackRef.current(event, context),
{
...optionsRef.current,
@@ -160,7 +175,7 @@ export function useHotkeySequence(
registrationRef.current = null
}
}
- }, [hotkeySequenceString, mergedOptions.enabled, sequence])
+ }, [hotkeySequenceString])
// Sync callback and options on EVERY render (outside useEffect)
if (registrationRef.current?.isActive) {
diff --git a/packages/preact-hotkeys/src/useHotkeySequences.ts b/packages/preact-hotkeys/src/useHotkeySequences.ts
new file mode 100644
index 00000000..13d048e6
--- /dev/null
+++ b/packages/preact-hotkeys/src/useHotkeySequences.ts
@@ -0,0 +1,206 @@
+import { useEffect, useRef } from 'preact/hooks'
+import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
+import { useDefaultHotkeysOptions } from './HotkeysProvider'
+import { isRef } from './utils'
+import type { UseHotkeySequenceOptions } from './useHotkeySequence'
+import type {
+ HotkeyCallback,
+ HotkeySequence,
+ SequenceRegistrationHandle,
+} from '@tanstack/hotkeys'
+
+/**
+ * A single sequence definition for use with `useHotkeySequences`.
+ */
+export interface UseHotkeySequenceDefinition {
+ /** Array of hotkey strings that form the sequence */
+ sequence: HotkeySequence
+ /** The function to call when the sequence is completed */
+ callback: HotkeyCallback
+ /** Per-sequence options (merged on top of commonOptions) */
+ options?: UseHotkeySequenceOptions
+}
+
+/**
+ * Preact hook for registering multiple keyboard shortcut sequences at once (Vim-style).
+ *
+ * Uses the singleton SequenceManager. Accepts a dynamic array of definitions so you can
+ * register variable-length lists without violating the rules of hooks.
+ *
+ * Options are merged in this order:
+ * HotkeysProvider defaults < commonOptions < per-definition options
+ *
+ * Callbacks and options are synced on every render to avoid stale closures.
+ *
+ * Definitions with an empty `sequence` are skipped (no registration).
+ *
+ * @param definitions - Array of sequence definitions to register
+ * @param commonOptions - Shared options applied to all sequences (overridden by per-definition options).
+ * Per-row `enabled: false` still registers that sequence: `SequenceManager` suppresses execution only (the row
+ * stays in the store and appears in TanStack Hotkeys devtools). Toggling `enabled` updates the existing handle
+ * via `setOptions` (no unregister/re-register churn).
+ *
+ * @example
+ * ```tsx
+ * function VimPalette() {
+ * useHotkeySequences([
+ * { sequence: ['G', 'G'], callback: () => scrollToTop() },
+ * { sequence: ['D', 'D'], callback: () => deleteLine() },
+ * { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord(), options: { timeout: 500 } },
+ * ])
+ * }
+ * ```
+ *
+ * @example
+ * ```tsx
+ * function DynamicSequences({ items }) {
+ * useHotkeySequences(
+ * items.map((item) => ({
+ * sequence: item.chords,
+ * callback: item.action,
+ * options: { enabled: item.enabled },
+ * })),
+ * { preventDefault: true },
+ * )
+ * }
+ * ```
+ */
+export function useHotkeySequences(
+ definitions: Array,
+ commonOptions: UseHotkeySequenceOptions = {},
+): void {
+ type RegistrationRecord = {
+ handle: SequenceRegistrationHandle
+ target: Document | HTMLElement | Window
+ }
+
+ const defaultOptions = useDefaultHotkeysOptions().hotkeySequence
+ const manager = getSequenceManager()
+
+ const registrationsRef = useRef>(new Map())
+ const definitionsRef = useRef(definitions)
+ const sequenceStringsRef = useRef>([])
+ const commonOptionsRef = useRef(commonOptions)
+ const defaultOptionsRef = useRef(defaultOptions)
+ const managerRef = useRef(manager)
+
+ const sequenceStrings = definitions.map((def) =>
+ formatHotkeySequence(def.sequence),
+ )
+
+ definitionsRef.current = definitions
+ sequenceStringsRef.current = sequenceStrings
+ commonOptionsRef.current = commonOptions
+ defaultOptionsRef.current = defaultOptions
+ managerRef.current = manager
+
+ useEffect(() => {
+ const prevRegistrations = registrationsRef.current
+ const nextRegistrations = new Map()
+
+ const rows: Array<{
+ registrationKey: string
+ def: (typeof definitionsRef.current)[number]
+ seq: HotkeySequence
+ seqStr: string
+ mergedOptions: UseHotkeySequenceOptions
+ resolvedTarget: Document | HTMLElement | Window
+ }> = []
+
+ for (let i = 0; i < definitionsRef.current.length; i++) {
+ const def = definitionsRef.current[i]!
+ const seqStr = sequenceStringsRef.current[i]!
+ const seq = def.sequence
+ if (seq.length === 0) {
+ continue
+ }
+
+ const mergedOptions = {
+ ...defaultOptionsRef.current,
+ ...commonOptionsRef.current,
+ ...def.options,
+ } as UseHotkeySequenceOptions
+
+ const resolvedTarget = isRef(mergedOptions.target)
+ ? mergedOptions.target.current
+ : (mergedOptions.target ??
+ (typeof document !== 'undefined' ? document : null))
+
+ if (!resolvedTarget) {
+ continue
+ }
+
+ const registrationKey = `${i}:${seqStr}`
+ rows.push({
+ registrationKey,
+ def,
+ seq,
+ seqStr,
+ mergedOptions,
+ resolvedTarget,
+ })
+ }
+
+ const nextKeys = new Set(rows.map((r) => r.registrationKey))
+
+ for (const [key, record] of prevRegistrations) {
+ if (!nextKeys.has(key) && record.handle.isActive) {
+ record.handle.unregister()
+ }
+ }
+
+ for (const row of rows) {
+ const { registrationKey, def, seq, mergedOptions, resolvedTarget } = row
+
+ const existing = prevRegistrations.get(registrationKey)
+ if (existing?.handle.isActive && existing.target === resolvedTarget) {
+ nextRegistrations.set(registrationKey, existing)
+ continue
+ }
+
+ if (existing?.handle.isActive) {
+ existing.handle.unregister()
+ }
+
+ const handle = managerRef.current.register(seq, def.callback, {
+ ...mergedOptions,
+ target: resolvedTarget,
+ })
+ nextRegistrations.set(registrationKey, {
+ handle,
+ target: resolvedTarget,
+ })
+ }
+
+ registrationsRef.current = nextRegistrations
+ })
+
+ useEffect(() => {
+ return () => {
+ for (const { handle } of registrationsRef.current.values()) {
+ if (handle.isActive) {
+ handle.unregister()
+ }
+ }
+ registrationsRef.current = new Map()
+ }
+ }, [])
+
+ for (let i = 0; i < definitions.length; i++) {
+ const def = definitions[i]!
+ const seqStr = sequenceStrings[i]!
+ const registrationKey = `${i}:${seqStr}`
+ const handle = registrationsRef.current.get(registrationKey)?.handle
+
+ if (handle?.isActive && def.sequence.length > 0) {
+ handle.callback = def.callback
+ const mergedOptions = {
+ ...defaultOptions,
+ ...commonOptions,
+ ...def.options,
+ } as UseHotkeySequenceOptions
+ const { target: _target, ...optionsWithoutTarget } = mergedOptions
+ handle.setOptions(optionsWithoutTarget)
+ }
+ }
+}
diff --git a/packages/preact-hotkeys/src/useHotkeys.ts b/packages/preact-hotkeys/src/useHotkeys.ts
index 25194500..5815dcfe 100644
--- a/packages/preact-hotkeys/src/useHotkeys.ts
+++ b/packages/preact-hotkeys/src/useHotkeys.ts
@@ -40,7 +40,10 @@ export interface UseHotkeyDefinition {
* Callbacks and options are synced on every render to avoid stale closures.
*
* @param hotkeys - Array of hotkey definitions to register
- * @param commonOptions - Shared options applied to all hotkeys (overridden by per-definition options)
+ * @param commonOptions - Shared options applied to all hotkeys (overridden by per-definition options).
+ * Per-row `enabled: false` still registers that hotkey: `HotkeyManager` suppresses execution only (the row
+ * stays in the store and appears in TanStack Hotkeys devtools). Toggling `enabled` updates the existing handle
+ * via `setOptions` (no unregister/re-register churn).
*
* @example
* ```tsx
@@ -101,18 +104,6 @@ export function useHotkeys(
defaultOptionsRef.current = defaultOptions
managerRef.current = manager
- const hotkeyKey = hotkeyStrings.join('\0')
- const enabledKey = hotkeys
- .map((def) => {
- const merged = {
- ...defaultOptions,
- ...commonOptions,
- ...def.options,
- }
- return merged.enabled ?? true
- })
- .join('\0')
-
useEffect(() => {
const prevRegistrations = registrationsRef.current
const nextRegistrations = new Map()
@@ -186,7 +177,9 @@ export function useHotkeys(
}
registrationsRef.current = nextRegistrations
+ })
+ useEffect(() => {
return () => {
for (const { handle } of registrationsRef.current.values()) {
if (handle.isActive) {
@@ -195,7 +188,7 @@ export function useHotkeys(
}
registrationsRef.current = new Map()
}
- }, [hotkeyKey, enabledKey])
+ }, [])
for (let i = 0; i < hotkeys.length; i++) {
const def = hotkeys[i]!
diff --git a/packages/preact-hotkeys/tests/useHotkey.test.tsx b/packages/preact-hotkeys/tests/useHotkey.test.tsx
index bcee773f..55cab25c 100644
--- a/packages/preact-hotkeys/tests/useHotkey.test.tsx
+++ b/packages/preact-hotkeys/tests/useHotkey.test.tsx
@@ -197,6 +197,32 @@ describe('useHotkey', () => {
)
expect(callback).toHaveBeenCalledTimes(2)
})
+
+ it('should preserve registration id when toggling enabled', () => {
+ const callback = vi.fn()
+ const manager = HotkeyManager.getInstance()
+
+ const { rerender } = render(
+ ,
+ )
+
+ const idBefore = [...manager.registrations.state.keys()][0]
+ expect(manager.getRegistrationCount()).toBe(1)
+ expect(idBefore).toBeDefined()
+
+ rerender( )
+ expect(manager.getRegistrationCount()).toBe(1)
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
+ expect(manager.registrations.state.get(idBefore!)?.options.enabled).toBe(
+ false,
+ )
+
+ rerender( )
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
+ expect(
+ manager.registrations.state.get(idBefore!)?.options.enabled,
+ ).not.toBe(false)
+ })
})
describe('target handling', () => {
diff --git a/packages/preact-hotkeys/tests/useHotkeySequences.test.tsx b/packages/preact-hotkeys/tests/useHotkeySequences.test.tsx
new file mode 100644
index 00000000..7e0a6997
--- /dev/null
+++ b/packages/preact-hotkeys/tests/useHotkeySequences.test.tsx
@@ -0,0 +1,234 @@
+// @vitest-environment happy-dom
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { cleanup, render } from '@testing-library/preact'
+import { SequenceManager } from '@tanstack/hotkeys'
+import { useHotkeySequences } from '../src/useHotkeySequences'
+import type { UseHotkeySequenceDefinition } from '../src/useHotkeySequences'
+
+function dispatchKey(key: string) {
+ document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
+}
+
+function SequencesComponent({
+ definitions,
+}: {
+ definitions: Array
+}) {
+ useHotkeySequences(definitions)
+ return null
+}
+
+describe('useHotkeySequences', () => {
+ beforeEach(() => {
+ SequenceManager.resetInstance()
+ })
+
+ afterEach(() => {
+ SequenceManager.resetInstance()
+ cleanup()
+ })
+
+ it('should register multiple sequence handlers', () => {
+ const a = vi.fn()
+ const b = vi.fn()
+
+ render(
+ ,
+ )
+
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(2)
+ })
+
+ it('should call the correct callback for each sequence', () => {
+ const gg = vi.fn()
+ const dd = vi.fn()
+
+ render(
+ ,
+ )
+
+ dispatchKey('g')
+ dispatchKey('g')
+ expect(gg).toHaveBeenCalledTimes(1)
+ expect(dd).not.toHaveBeenCalled()
+
+ dispatchKey('d')
+ dispatchKey('d')
+ expect(gg).toHaveBeenCalledTimes(1)
+ expect(dd).toHaveBeenCalledTimes(1)
+ })
+
+ it('should unregister all sequences on unmount', () => {
+ const { unmount } = render(
+ ,
+ )
+
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(2)
+ unmount()
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(0)
+ })
+
+ it('should handle an empty array as a no-op', () => {
+ render( )
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(0)
+ })
+
+ it('should skip definitions with an empty sequence', () => {
+ render(
+ ,
+ )
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(1)
+ })
+
+ it('should register disabled sequences and keep them in the manager', () => {
+ const enabledCb = vi.fn()
+ const disabledCb = vi.fn()
+
+ render(
+ ,
+ )
+
+ dispatchKey('g')
+ dispatchKey('g')
+ dispatchKey('d')
+ dispatchKey('d')
+ expect(enabledCb).toHaveBeenCalledTimes(1)
+ expect(disabledCb).not.toHaveBeenCalled()
+
+ const manager = SequenceManager.getInstance()
+ expect(manager.getRegistrationCount()).toBe(2)
+ const disabledView = [...manager.registrations.state.values()].find(
+ (r) => r.sequence[0] === 'D' && r.sequence[1] === 'D',
+ )
+ expect(disabledView?.options.enabled).toBe(false)
+ })
+
+ it('should move a sequence registration when only the target changes', () => {
+ const callback = vi.fn()
+ const targetA = document.createElement('div')
+ const targetB = document.createElement('div')
+
+ const { rerender } = render(
+ ,
+ )
+
+ targetA.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'g', bubbles: true }),
+ )
+ targetA.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'g', bubbles: true }),
+ )
+ expect(callback).toHaveBeenCalledTimes(1)
+
+ rerender(
+ ,
+ )
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(1)
+
+ targetA.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'g', bubbles: true }),
+ )
+ targetA.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'g', bubbles: true }),
+ )
+ expect(callback).toHaveBeenCalledTimes(1)
+
+ targetB.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'g', bubbles: true }),
+ )
+ targetB.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'g', bubbles: true }),
+ )
+ expect(callback).toHaveBeenCalledTimes(2)
+ })
+
+ describe('stale closure prevention', () => {
+ it('should sync enabled option on every render', () => {
+ const callback = vi.fn()
+
+ function EnabledSequences({ enabled }: { enabled: boolean }) {
+ useHotkeySequences([
+ { sequence: ['G', 'G'], callback, options: { enabled } },
+ ])
+ return null
+ }
+
+ const { rerender } = render( )
+
+ dispatchKey('g')
+ dispatchKey('g')
+ expect(callback).toHaveBeenCalledTimes(1)
+
+ rerender( )
+ dispatchKey('g')
+ dispatchKey('g')
+ expect(callback).toHaveBeenCalledTimes(1)
+
+ rerender( )
+ dispatchKey('g')
+ dispatchKey('g')
+ expect(callback).toHaveBeenCalledTimes(2)
+ })
+
+ it('should preserve registration id when toggling enabled', () => {
+ const callback = vi.fn()
+ const manager = SequenceManager.getInstance()
+
+ function EnabledSequences({ enabled }: { enabled: boolean }) {
+ useHotkeySequences([
+ { sequence: ['G', 'G'], callback, options: { enabled } },
+ ])
+ return null
+ }
+
+ const { rerender } = render( )
+
+ const idBefore = [...manager.registrations.state.keys()][0]
+ expect(manager.getRegistrationCount()).toBe(1)
+
+ rerender( )
+ expect(manager.getRegistrationCount()).toBe(1)
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
+
+ rerender( )
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
+ })
+ })
+})
diff --git a/packages/preact-hotkeys/tests/useHotkeys.test.tsx b/packages/preact-hotkeys/tests/useHotkeys.test.tsx
index 687cff08..803bfaee 100644
--- a/packages/preact-hotkeys/tests/useHotkeys.test.tsx
+++ b/packages/preact-hotkeys/tests/useHotkeys.test.tsx
@@ -210,6 +210,63 @@ describe('useHotkeys', () => {
expect(enabledCb).toHaveBeenCalledTimes(1)
expect(disabledCb).not.toHaveBeenCalled()
+
+ const manager = HotkeyManager.getInstance()
+ expect(manager.getRegistrationCount()).toBe(2)
+ const disabledReg = [...manager.registrations.state.values()].find(
+ (r) => r.hotkey === 'Mod+Z',
+ )
+ expect(disabledReg?.options.enabled).toBe(false)
+ })
+
+ it('should move a registration when only the target changes', () => {
+ const callback = vi.fn()
+ const targetA = document.createElement('div')
+ const targetB = document.createElement('div')
+
+ const { rerender } = render(
+ ,
+ )
+
+ targetA.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 's',
+ metaKey: true,
+ bubbles: true,
+ }),
+ )
+ expect(callback).toHaveBeenCalledTimes(1)
+
+ rerender(
+ ,
+ )
+ expect(HotkeyManager.getInstance().getRegistrationCount()).toBe(1)
+
+ targetA.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 's',
+ metaKey: true,
+ bubbles: true,
+ }),
+ )
+ expect(callback).toHaveBeenCalledTimes(1)
+
+ targetB.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 's',
+ metaKey: true,
+ bubbles: true,
+ }),
+ )
+ expect(callback).toHaveBeenCalledTimes(2)
})
describe('stale closure prevention', () => {
@@ -311,5 +368,29 @@ describe('useHotkeys', () => {
)
expect(callback).toHaveBeenCalledTimes(2)
})
+
+ it('should preserve registration id when toggling enabled', () => {
+ const callback = vi.fn()
+ const manager = HotkeyManager.getInstance()
+
+ function EnabledComponent({ enabled }: { enabled: boolean }) {
+ useHotkeys([{ hotkey: 'Mod+S', callback, options: { enabled } }], {
+ platform: 'mac',
+ })
+ return null
+ }
+
+ const { rerender } = render( )
+
+ const idBefore = [...manager.registrations.state.keys()][0]
+ expect(manager.getRegistrationCount()).toBe(1)
+
+ rerender( )
+ expect(manager.getRegistrationCount()).toBe(1)
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
+
+ rerender( )
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
+ })
})
})
diff --git a/packages/react-hotkeys/src/index.ts b/packages/react-hotkeys/src/index.ts
index d33cdc82..a89024f7 100644
--- a/packages/react-hotkeys/src/index.ts
+++ b/packages/react-hotkeys/src/index.ts
@@ -11,5 +11,6 @@ export * from './useHeldKeys'
export * from './useHeldKeyCodes'
export * from './useKeyHold'
export * from './useHotkeySequence'
+export * from './useHotkeySequences'
export * from './useHotkeyRecorder'
export * from './useHotkeySequenceRecorder'
diff --git a/packages/react-hotkeys/src/useHotkey.ts b/packages/react-hotkeys/src/useHotkey.ts
index 385f5eab..1fbab9b1 100644
--- a/packages/react-hotkeys/src/useHotkey.ts
+++ b/packages/react-hotkeys/src/useHotkey.ts
@@ -43,7 +43,8 @@ export interface UseHotkeyOptions extends Omit {
*
* @param hotkey - The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object (supports `mod` for cross-platform)
* @param callback - The function to call when the hotkey is pressed
- * @param options - Options for the hotkey behavior
+ * @param options - Options for the hotkey behavior. `enabled: false` keeps the registration (visible in devtools)
+ * and only suppresses firing; the hook updates the existing handle instead of unregistering.
*
* @example
* ```tsx
@@ -135,6 +136,12 @@ export function useHotkey(
// Skip if no valid target (SSR or ref still null)
if (!resolvedTarget) {
+ if (registrationRef.current?.isActive) {
+ registrationRef.current.unregister()
+ registrationRef.current = null
+ }
+ prevTargetRef.current = null
+ prevHotkeyRef.current = null
return
}
@@ -174,7 +181,7 @@ export function useHotkey(
registrationRef.current = null
}
}
- }, [hotkeyString, options.enabled])
+ }, [hotkeyString])
// Sync callback and options on EVERY render (outside useEffect)
// This avoids stale closures - the callback always has access to latest state
diff --git a/packages/react-hotkeys/src/useHotkeySequence.ts b/packages/react-hotkeys/src/useHotkeySequence.ts
index 2084b71e..ab004109 100644
--- a/packages/react-hotkeys/src/useHotkeySequence.ts
+++ b/packages/react-hotkeys/src/useHotkeySequence.ts
@@ -40,7 +40,8 @@ export interface UseHotkeySequenceOptions extends Omit<
*
* @param sequence - Array of hotkey strings that form the sequence
* @param callback - Function to call when the sequence is completed
- * @param options - Options for the sequence behavior
+ * @param options - Options for the sequence behavior. `enabled: false` keeps the registration (visible in devtools)
+ * and only suppresses firing; the hook updates the existing handle instead of unregistering.
*
* @example
* ```tsx
@@ -108,6 +109,12 @@ export function useHotkeySequence(
useEffect(() => {
if (sequenceRef.current.length === 0) {
+ if (registrationRef.current?.isActive) {
+ registrationRef.current.unregister()
+ registrationRef.current = null
+ }
+ prevTargetRef.current = null
+ prevSequenceRef.current = null
return
}
@@ -119,6 +126,12 @@ export function useHotkeySequence(
// Skip if no valid target (SSR or ref still null)
if (!resolvedTarget) {
+ if (registrationRef.current?.isActive) {
+ registrationRef.current.unregister()
+ registrationRef.current = null
+ }
+ prevTargetRef.current = null
+ prevSequenceRef.current = null
return
}
@@ -161,7 +174,7 @@ export function useHotkeySequence(
registrationRef.current = null
}
}
- }, [hotkeySequenceString, mergedOptions.enabled])
+ }, [hotkeySequenceString])
// Sync callback and options on EVERY render (outside useEffect)
if (registrationRef.current?.isActive) {
diff --git a/packages/react-hotkeys/src/useHotkeySequences.ts b/packages/react-hotkeys/src/useHotkeySequences.ts
new file mode 100644
index 00000000..467fabdb
--- /dev/null
+++ b/packages/react-hotkeys/src/useHotkeySequences.ts
@@ -0,0 +1,206 @@
+import { useEffect, useRef } from 'react'
+import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
+import { useDefaultHotkeysOptions } from './HotkeysProvider'
+import { isRef } from './utils'
+import type { UseHotkeySequenceOptions } from './useHotkeySequence'
+import type {
+ HotkeyCallback,
+ HotkeySequence,
+ SequenceRegistrationHandle,
+} from '@tanstack/hotkeys'
+
+/**
+ * A single sequence definition for use with `useHotkeySequences`.
+ */
+export interface UseHotkeySequenceDefinition {
+ /** Array of hotkey strings that form the sequence */
+ sequence: HotkeySequence
+ /** The function to call when the sequence is completed */
+ callback: HotkeyCallback
+ /** Per-sequence options (merged on top of commonOptions) */
+ options?: UseHotkeySequenceOptions
+}
+
+/**
+ * React hook for registering multiple keyboard shortcut sequences at once (Vim-style).
+ *
+ * Uses the singleton SequenceManager. Accepts a dynamic array of definitions so you can
+ * register variable-length lists without violating the rules of hooks.
+ *
+ * Options are merged in this order:
+ * HotkeysProvider defaults < commonOptions < per-definition options
+ *
+ * Callbacks and options are synced on every render to avoid stale closures.
+ *
+ * Definitions with an empty `sequence` are skipped (no registration).
+ *
+ * @param definitions - Array of sequence definitions to register
+ * @param commonOptions - Shared options applied to all sequences (overridden by per-definition options).
+ * Per-row `enabled: false` still registers that sequence: `SequenceManager` suppresses execution only (the row
+ * stays in the store and appears in TanStack Hotkeys devtools). Toggling `enabled` updates the existing handle
+ * via `setOptions` (no unregister/re-register churn).
+ *
+ * @example
+ * ```tsx
+ * function VimPalette() {
+ * useHotkeySequences([
+ * { sequence: ['G', 'G'], callback: () => scrollToTop() },
+ * { sequence: ['D', 'D'], callback: () => deleteLine() },
+ * { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord(), options: { timeout: 500 } },
+ * ])
+ * }
+ * ```
+ *
+ * @example
+ * ```tsx
+ * function DynamicSequences({ items }) {
+ * useHotkeySequences(
+ * items.map((item) => ({
+ * sequence: item.chords,
+ * callback: item.action,
+ * options: { enabled: item.enabled },
+ * })),
+ * { preventDefault: true },
+ * )
+ * }
+ * ```
+ */
+export function useHotkeySequences(
+ definitions: Array,
+ commonOptions: UseHotkeySequenceOptions = {},
+): void {
+ type RegistrationRecord = {
+ handle: SequenceRegistrationHandle
+ target: Document | HTMLElement | Window
+ }
+
+ const defaultOptions = useDefaultHotkeysOptions().hotkeySequence
+ const manager = getSequenceManager()
+
+ const registrationsRef = useRef>(new Map())
+ const definitionsRef = useRef(definitions)
+ const sequenceStringsRef = useRef>([])
+ const commonOptionsRef = useRef(commonOptions)
+ const defaultOptionsRef = useRef(defaultOptions)
+ const managerRef = useRef(manager)
+
+ const sequenceStrings = definitions.map((def) =>
+ formatHotkeySequence(def.sequence),
+ )
+
+ definitionsRef.current = definitions
+ sequenceStringsRef.current = sequenceStrings
+ commonOptionsRef.current = commonOptions
+ defaultOptionsRef.current = defaultOptions
+ managerRef.current = manager
+
+ useEffect(() => {
+ const prevRegistrations = registrationsRef.current
+ const nextRegistrations = new Map()
+
+ const rows: Array<{
+ registrationKey: string
+ def: (typeof definitionsRef.current)[number]
+ seq: HotkeySequence
+ seqStr: string
+ mergedOptions: UseHotkeySequenceOptions
+ resolvedTarget: Document | HTMLElement | Window
+ }> = []
+
+ for (let i = 0; i < definitionsRef.current.length; i++) {
+ const def = definitionsRef.current[i]!
+ const seqStr = sequenceStringsRef.current[i]!
+ const seq = def.sequence
+ if (seq.length === 0) {
+ continue
+ }
+
+ const mergedOptions = {
+ ...defaultOptionsRef.current,
+ ...commonOptionsRef.current,
+ ...def.options,
+ } as UseHotkeySequenceOptions
+
+ const resolvedTarget = isRef(mergedOptions.target)
+ ? mergedOptions.target.current
+ : (mergedOptions.target ??
+ (typeof document !== 'undefined' ? document : null))
+
+ if (!resolvedTarget) {
+ continue
+ }
+
+ const registrationKey = `${i}:${seqStr}`
+ rows.push({
+ registrationKey,
+ def,
+ seq,
+ seqStr,
+ mergedOptions,
+ resolvedTarget,
+ })
+ }
+
+ const nextKeys = new Set(rows.map((r) => r.registrationKey))
+
+ for (const [key, record] of prevRegistrations) {
+ if (!nextKeys.has(key) && record.handle.isActive) {
+ record.handle.unregister()
+ }
+ }
+
+ for (const row of rows) {
+ const { registrationKey, def, seq, mergedOptions, resolvedTarget } = row
+
+ const existing = prevRegistrations.get(registrationKey)
+ if (existing?.handle.isActive && existing.target === resolvedTarget) {
+ nextRegistrations.set(registrationKey, existing)
+ continue
+ }
+
+ if (existing?.handle.isActive) {
+ existing.handle.unregister()
+ }
+
+ const handle = managerRef.current.register(seq, def.callback, {
+ ...mergedOptions,
+ target: resolvedTarget,
+ })
+ nextRegistrations.set(registrationKey, {
+ handle,
+ target: resolvedTarget,
+ })
+ }
+
+ registrationsRef.current = nextRegistrations
+ })
+
+ useEffect(() => {
+ return () => {
+ for (const { handle } of registrationsRef.current.values()) {
+ if (handle.isActive) {
+ handle.unregister()
+ }
+ }
+ registrationsRef.current = new Map()
+ }
+ }, [])
+
+ for (let i = 0; i < definitions.length; i++) {
+ const def = definitions[i]!
+ const seqStr = sequenceStrings[i]!
+ const registrationKey = `${i}:${seqStr}`
+ const handle = registrationsRef.current.get(registrationKey)?.handle
+
+ if (handle?.isActive && def.sequence.length > 0) {
+ handle.callback = def.callback
+ const mergedOptions = {
+ ...defaultOptions,
+ ...commonOptions,
+ ...def.options,
+ } as UseHotkeySequenceOptions
+ const { target: _target, ...optionsWithoutTarget } = mergedOptions
+ handle.setOptions(optionsWithoutTarget)
+ }
+ }
+}
diff --git a/packages/react-hotkeys/src/useHotkeys.ts b/packages/react-hotkeys/src/useHotkeys.ts
index 9265deb4..11f45cae 100644
--- a/packages/react-hotkeys/src/useHotkeys.ts
+++ b/packages/react-hotkeys/src/useHotkeys.ts
@@ -40,7 +40,10 @@ export interface UseHotkeyDefinition {
* Callbacks and options are synced on every render to avoid stale closures.
*
* @param hotkeys - Array of hotkey definitions to register
- * @param commonOptions - Shared options applied to all hotkeys (overridden by per-definition options)
+ * @param commonOptions - Shared options applied to all hotkeys (overridden by per-definition options).
+ * Per-row `enabled: false` still registers that hotkey: `HotkeyManager` suppresses execution only (the row
+ * stays in the store and appears in TanStack Hotkeys devtools). Toggling `enabled` updates the existing handle
+ * via `setOptions` (no unregister/re-register churn).
*
* @example
* ```tsx
@@ -101,19 +104,6 @@ export function useHotkeys(
defaultOptionsRef.current = defaultOptions
managerRef.current = manager
- // Stable serialized keys for effect dependencies
- const hotkeyKey = hotkeyStrings.join('\0')
- const enabledKey = hotkeys
- .map((def) => {
- const merged = {
- ...defaultOptions,
- ...commonOptions,
- ...def.options,
- }
- return merged.enabled ?? true
- })
- .join('\0')
-
useEffect(() => {
const prevRegistrations = registrationsRef.current
const nextRegistrations = new Map()
@@ -187,7 +177,9 @@ export function useHotkeys(
}
registrationsRef.current = nextRegistrations
+ })
+ useEffect(() => {
return () => {
for (const { handle } of registrationsRef.current.values()) {
if (handle.isActive) {
@@ -196,7 +188,7 @@ export function useHotkeys(
}
registrationsRef.current = new Map()
}
- }, [hotkeyKey, enabledKey])
+ }, [])
// Sync callbacks and options on EVERY render (outside useEffect)
for (let i = 0; i < hotkeys.length; i++) {
diff --git a/packages/react-hotkeys/tests/useHotkey.test.tsx b/packages/react-hotkeys/tests/useHotkey.test.tsx
index b9ec5b5c..03da08c8 100644
--- a/packages/react-hotkeys/tests/useHotkey.test.tsx
+++ b/packages/react-hotkeys/tests/useHotkey.test.tsx
@@ -203,6 +203,34 @@ describe('useHotkey', () => {
)
expect(callback).toHaveBeenCalledTimes(2)
})
+
+ it('should preserve registration id when toggling enabled', () => {
+ const callback = vi.fn()
+ const manager = HotkeyManager.getInstance()
+
+ const { rerender } = renderHook(
+ ({ enabled }: { enabled: boolean }) =>
+ useHotkey('Mod+S', callback, { platform: 'mac', enabled }),
+ { initialProps: { enabled: true } },
+ )
+
+ const idBefore = [...manager.registrations.state.keys()][0]
+ expect(manager.getRegistrationCount()).toBe(1)
+ expect(idBefore).toBeDefined()
+
+ rerender({ enabled: false })
+ expect(manager.getRegistrationCount()).toBe(1)
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
+ expect(manager.registrations.state.get(idBefore!)?.options.enabled).toBe(
+ false,
+ )
+
+ rerender({ enabled: true })
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
+ expect(
+ manager.registrations.state.get(idBefore!)?.options.enabled,
+ ).not.toBe(false)
+ })
})
describe('target handling', () => {
diff --git a/packages/react-hotkeys/tests/useHotkeySequences.test.tsx b/packages/react-hotkeys/tests/useHotkeySequences.test.tsx
new file mode 100644
index 00000000..03f264c8
--- /dev/null
+++ b/packages/react-hotkeys/tests/useHotkeySequences.test.tsx
@@ -0,0 +1,283 @@
+// @vitest-environment happy-dom
+import { act, renderHook } from '@testing-library/react'
+import { SequenceManager } from '@tanstack/hotkeys'
+import { useState } from 'react'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { useHotkeySequences } from '../src/useHotkeySequences'
+import type { UseHotkeySequenceDefinition } from '../src/useHotkeySequences'
+
+function dispatchKey(key: string) {
+ document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
+}
+
+describe('useHotkeySequences', () => {
+ beforeEach(() => {
+ SequenceManager.resetInstance()
+ })
+
+ afterEach(() => {
+ SequenceManager.resetInstance()
+ })
+
+ it('should register multiple sequence handlers', () => {
+ const a = vi.fn()
+ const b = vi.fn()
+
+ renderHook(() =>
+ useHotkeySequences([
+ { sequence: ['G', 'G'], callback: a },
+ { sequence: ['D', 'D'], callback: b },
+ ]),
+ )
+
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(2)
+ })
+
+ it('should call the correct callback for each sequence', () => {
+ const gg = vi.fn()
+ const dd = vi.fn()
+
+ renderHook(() =>
+ useHotkeySequences([
+ { sequence: ['G', 'G'], callback: gg },
+ { sequence: ['D', 'D'], callback: dd },
+ ]),
+ )
+
+ dispatchKey('g')
+ dispatchKey('g')
+ expect(gg).toHaveBeenCalledTimes(1)
+ expect(dd).not.toHaveBeenCalled()
+
+ dispatchKey('d')
+ dispatchKey('d')
+ expect(gg).toHaveBeenCalledTimes(1)
+ expect(dd).toHaveBeenCalledTimes(1)
+ })
+
+ it('should unregister all sequences on unmount', () => {
+ const { unmount } = renderHook(() =>
+ useHotkeySequences([
+ { sequence: ['G', 'G'], callback: vi.fn() },
+ { sequence: ['D', 'D'], callback: vi.fn() },
+ ]),
+ )
+
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(2)
+ unmount()
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(0)
+ })
+
+ it('should handle an empty array as a no-op', () => {
+ renderHook(() => useHotkeySequences([]))
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(0)
+ })
+
+ it('should skip definitions with an empty sequence', () => {
+ renderHook(() =>
+ useHotkeySequences([
+ { sequence: [], callback: vi.fn() },
+ { sequence: ['G', 'G'], callback: vi.fn() },
+ ]),
+ )
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(1)
+ })
+
+ it('should register disabled sequences and keep them in the manager', () => {
+ const enabledCb = vi.fn()
+ const disabledCb = vi.fn()
+
+ renderHook(() =>
+ useHotkeySequences([
+ { sequence: ['G', 'G'], callback: enabledCb },
+ {
+ sequence: ['D', 'D'],
+ callback: disabledCb,
+ options: { enabled: false },
+ },
+ ]),
+ )
+
+ dispatchKey('g')
+ dispatchKey('g')
+ dispatchKey('d')
+ dispatchKey('d')
+ expect(enabledCb).toHaveBeenCalledTimes(1)
+ expect(disabledCb).not.toHaveBeenCalled()
+
+ const manager = SequenceManager.getInstance()
+ expect(manager.getRegistrationCount()).toBe(2)
+ const disabledView = [...manager.registrations.state.values()].find(
+ (r) => r.sequence[0] === 'D' && r.sequence[1] === 'D',
+ )
+ expect(disabledView?.options.enabled).toBe(false)
+ })
+
+ it('should handle dynamic array changes (add sequence)', () => {
+ const gg = vi.fn()
+ const dd = vi.fn()
+ const yy = vi.fn()
+
+ const { rerender } = renderHook(
+ ({ defs }: { defs: Array }) =>
+ useHotkeySequences(defs),
+ {
+ initialProps: {
+ defs: [
+ { sequence: ['G', 'G'], callback: gg },
+ { sequence: ['D', 'D'], callback: dd },
+ ],
+ },
+ },
+ )
+
+ dispatchKey('y')
+ dispatchKey('y')
+ expect(yy).not.toHaveBeenCalled()
+
+ rerender({
+ defs: [
+ { sequence: ['G', 'G'], callback: gg },
+ { sequence: ['D', 'D'], callback: dd },
+ { sequence: ['Y', 'Y'], callback: yy },
+ ],
+ })
+
+ dispatchKey('y')
+ dispatchKey('y')
+ expect(yy).toHaveBeenCalledTimes(1)
+ })
+
+ it('should move a sequence registration when only the target changes', () => {
+ const callback = vi.fn()
+ const targetA = document.createElement('div')
+ const targetB = document.createElement('div')
+
+ const { rerender } = renderHook(
+ ({ target }: { target: HTMLElement }) =>
+ useHotkeySequences([
+ { sequence: ['G', 'G'], callback, options: { target } },
+ ]),
+ { initialProps: { target: targetA } },
+ )
+
+ targetA.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'g', bubbles: true }),
+ )
+ targetA.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'g', bubbles: true }),
+ )
+ expect(callback).toHaveBeenCalledTimes(1)
+
+ rerender({ target: targetB })
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(1)
+
+ targetA.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'g', bubbles: true }),
+ )
+ targetA.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'g', bubbles: true }),
+ )
+ expect(callback).toHaveBeenCalledTimes(1)
+
+ targetB.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'g', bubbles: true }),
+ )
+ targetB.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'g', bubbles: true }),
+ )
+ expect(callback).toHaveBeenCalledTimes(2)
+ })
+
+ describe('stale closure prevention', () => {
+ it('should have access to latest state values in callbacks', () => {
+ const capturedValues: Array = []
+
+ const { result, rerender } = renderHook(() => {
+ const [count, setCount] = useState(0)
+ useHotkeySequences([
+ {
+ sequence: ['G', 'G'],
+ callback: () => {
+ capturedValues.push(count)
+ },
+ },
+ ])
+ return { setCount }
+ })
+
+ dispatchKey('g')
+ dispatchKey('g')
+ expect(capturedValues).toEqual([0])
+
+ act(() => {
+ result.current.setCount(5)
+ })
+ rerender()
+
+ dispatchKey('g')
+ dispatchKey('g')
+ expect(capturedValues).toEqual([0, 5])
+
+ act(() => {
+ result.current.setCount(10)
+ })
+ rerender()
+
+ dispatchKey('g')
+ dispatchKey('g')
+ expect(capturedValues).toEqual([0, 5, 10])
+ })
+
+ it('should sync enabled option on every render', () => {
+ const callback = vi.fn()
+
+ const { rerender } = renderHook(
+ ({ enabled }: { enabled: boolean }) =>
+ useHotkeySequences([
+ { sequence: ['G', 'G'], callback, options: { enabled } },
+ ]),
+ { initialProps: { enabled: true } },
+ )
+
+ dispatchKey('g')
+ dispatchKey('g')
+ expect(callback).toHaveBeenCalledTimes(1)
+
+ rerender({ enabled: false })
+
+ dispatchKey('g')
+ dispatchKey('g')
+ expect(callback).toHaveBeenCalledTimes(1)
+
+ rerender({ enabled: true })
+
+ dispatchKey('g')
+ dispatchKey('g')
+ expect(callback).toHaveBeenCalledTimes(2)
+ })
+
+ it('should preserve registration id when toggling enabled', () => {
+ const callback = vi.fn()
+ const manager = SequenceManager.getInstance()
+
+ const { rerender } = renderHook(
+ ({ enabled }: { enabled: boolean }) =>
+ useHotkeySequences([
+ { sequence: ['G', 'G'], callback, options: { enabled } },
+ ]),
+ { initialProps: { enabled: true } },
+ )
+
+ const idBefore = [...manager.registrations.state.keys()][0]
+ expect(manager.getRegistrationCount()).toBe(1)
+
+ rerender({ enabled: false })
+ expect(manager.getRegistrationCount()).toBe(1)
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
+
+ rerender({ enabled: true })
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
+ })
+ })
+})
diff --git a/packages/react-hotkeys/tests/useHotkeys.test.tsx b/packages/react-hotkeys/tests/useHotkeys.test.tsx
index abfa1e0e..ae8a1d08 100644
--- a/packages/react-hotkeys/tests/useHotkeys.test.tsx
+++ b/packages/react-hotkeys/tests/useHotkeys.test.tsx
@@ -223,6 +223,57 @@ describe('useHotkeys', () => {
expect(enabledCb).toHaveBeenCalledTimes(1)
expect(disabledCb).not.toHaveBeenCalled()
+
+ const manager = HotkeyManager.getInstance()
+ expect(manager.getRegistrationCount()).toBe(2)
+ const disabledReg = [...manager.registrations.state.values()].find(
+ (r) => r.hotkey === 'Mod+Z',
+ )
+ expect(disabledReg?.options.enabled).toBe(false)
+ })
+
+ it('should move a registration when only the target changes', () => {
+ const callback = vi.fn()
+ const targetA = document.createElement('div')
+ const targetB = document.createElement('div')
+
+ const { rerender } = renderHook(
+ ({ target }: { target: HTMLElement }) =>
+ useHotkeys([{ hotkey: 'Mod+S', callback, options: { target } }], {
+ platform: 'mac',
+ }),
+ { initialProps: { target: targetA } },
+ )
+
+ targetA.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 's',
+ metaKey: true,
+ bubbles: true,
+ }),
+ )
+ expect(callback).toHaveBeenCalledTimes(1)
+
+ rerender({ target: targetB })
+ expect(HotkeyManager.getInstance().getRegistrationCount()).toBe(1)
+
+ targetA.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 's',
+ metaKey: true,
+ bubbles: true,
+ }),
+ )
+ expect(callback).toHaveBeenCalledTimes(1)
+
+ targetB.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 's',
+ metaKey: true,
+ bubbles: true,
+ }),
+ )
+ expect(callback).toHaveBeenCalledTimes(2)
})
describe('stale closure prevention', () => {
@@ -327,5 +378,28 @@ describe('useHotkeys', () => {
)
expect(callback).toHaveBeenCalledTimes(2)
})
+
+ it('should preserve registration id when toggling enabled', () => {
+ const callback = vi.fn()
+ const manager = HotkeyManager.getInstance()
+
+ const { rerender } = renderHook(
+ ({ enabled }: { enabled: boolean }) =>
+ useHotkeys([{ hotkey: 'Mod+S', callback, options: { enabled } }], {
+ platform: 'mac',
+ }),
+ { initialProps: { enabled: true } },
+ )
+
+ const idBefore = [...manager.registrations.state.keys()][0]
+ expect(manager.getRegistrationCount()).toBe(1)
+
+ rerender({ enabled: false })
+ expect(manager.getRegistrationCount()).toBe(1)
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
+
+ rerender({ enabled: true })
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
+ })
})
})
diff --git a/packages/solid-hotkeys/src/createHotkey.ts b/packages/solid-hotkeys/src/createHotkey.ts
index 1543888b..8b4fb10c 100644
--- a/packages/solid-hotkeys/src/createHotkey.ts
+++ b/packages/solid-hotkeys/src/createHotkey.ts
@@ -89,6 +89,17 @@ export function createHotkey(
const manager = getHotkeyManager()
let registration: HotkeyRegistrationHandle | null = null
+ let lastHotkeyString: Hotkey | null = null
+ let lastTarget: HTMLElement | Document | Window | null = null
+
+ onCleanup(() => {
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
+ }
+ lastHotkeyString = null
+ lastTarget = null
+ })
createEffect(() => {
// Resolve reactive values
@@ -119,36 +130,44 @@ export function createHotkey(
: null
if (!resolvedTarget) {
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
+ }
+ lastHotkeyString = null
+ lastTarget = null
+ return
+ }
+
+ // Extract options without target (target is handled separately)
+ const { target: _target, ...optionsWithoutTarget } = mergedOptions
+
+ if (
+ registration?.isActive &&
+ lastHotkeyString === hotkeyString &&
+ lastTarget === resolvedTarget
+ ) {
+ registration.callback = callback
+ registration.setOptions(optionsWithoutTarget)
return
}
- // Unregister previous registration if it exists
if (registration?.isActive) {
registration.unregister()
registration = null
}
- // Extract options without target (target is handled separately)
- const { target: _target, ...optionsWithoutTarget } = mergedOptions
-
- // Register the hotkey
registration = manager.register(hotkeyString, callback, {
...optionsWithoutTarget,
target: resolvedTarget,
})
- // Update callback and options on every effect run
if (registration.isActive) {
registration.callback = callback
registration.setOptions(optionsWithoutTarget)
}
- // Cleanup on disposal
- onCleanup(() => {
- if (registration?.isActive) {
- registration.unregister()
- registration = null
- }
- })
+ lastHotkeyString = hotkeyString
+ lastTarget = resolvedTarget
})
}
diff --git a/packages/solid-hotkeys/src/createHotkeySequence.ts b/packages/solid-hotkeys/src/createHotkeySequence.ts
index 22273019..79536507 100644
--- a/packages/solid-hotkeys/src/createHotkeySequence.ts
+++ b/packages/solid-hotkeys/src/createHotkeySequence.ts
@@ -1,5 +1,5 @@
import { createEffect, onCleanup } from 'solid-js'
-import { getSequenceManager } from '@tanstack/hotkeys'
+import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
import { useDefaultHotkeysOptions } from './HotkeysProvider'
import type {
HotkeyCallback,
@@ -72,6 +72,17 @@ export function createHotkeySequence(
const manager = getSequenceManager()
let registration: SequenceRegistrationHandle | null = null
+ let lastSequenceKey: string | null = null
+ let lastTarget: HTMLElement | Document | Window | null = null
+
+ onCleanup(() => {
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
+ }
+ lastSequenceKey = null
+ lastTarget = null
+ })
createEffect(() => {
// Resolve reactive values
@@ -87,10 +98,6 @@ export function createHotkeySequence(
// Extract options without target (target is handled separately)
const { target: _target, ...optionsWithoutTarget } = mergedOptions
- if (resolvedSequence.length === 0) {
- return
- }
-
// Resolve target: when explicitly provided (even as null), use it and skip if null.
// When not provided, default to document. Matches createHotkey.
const resolvedTarget =
@@ -100,34 +107,44 @@ export function createHotkeySequence(
? document
: null
- if (!resolvedTarget) {
+ if (resolvedSequence.length === 0 || !resolvedTarget) {
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
+ }
+ lastSequenceKey = null
+ lastTarget = null
+ return
+ }
+
+ const sequenceKey = formatHotkeySequence(resolvedSequence)
+
+ if (
+ registration?.isActive &&
+ lastSequenceKey === sequenceKey &&
+ lastTarget === resolvedTarget
+ ) {
+ registration.callback = callback
+ registration.setOptions(optionsWithoutTarget)
return
}
- // Unregister previous registration if it exists
if (registration?.isActive) {
registration.unregister()
registration = null
}
- // Register the sequence
registration = manager.register(resolvedSequence, callback, {
...mergedOptions,
target: resolvedTarget,
})
- // Sync callback and options on every effect run
if (registration.isActive) {
registration.callback = callback
registration.setOptions(optionsWithoutTarget)
}
- // Cleanup on disposal
- onCleanup(() => {
- if (registration?.isActive) {
- registration.unregister()
- registration = null
- }
- })
+ lastSequenceKey = sequenceKey
+ lastTarget = resolvedTarget
})
}
diff --git a/packages/solid-hotkeys/src/createHotkeySequences.ts b/packages/solid-hotkeys/src/createHotkeySequences.ts
new file mode 100644
index 00000000..6b259117
--- /dev/null
+++ b/packages/solid-hotkeys/src/createHotkeySequences.ts
@@ -0,0 +1,176 @@
+import { createEffect, onCleanup } from 'solid-js'
+import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
+import { useDefaultHotkeysOptions } from './HotkeysProvider'
+import type { CreateHotkeySequenceOptions } from './createHotkeySequence'
+import type {
+ HotkeyCallback,
+ HotkeySequence,
+ SequenceRegistrationHandle,
+} from '@tanstack/hotkeys'
+
+/**
+ * A single sequence definition for use with `createHotkeySequences`.
+ */
+export interface CreateHotkeySequenceDefinition {
+ /** Array of hotkey strings that form the sequence */
+ sequence: HotkeySequence
+ /** The function to call when the sequence is completed */
+ callback: HotkeyCallback
+ /** Per-sequence options (merged on top of commonOptions) */
+ options?: CreateHotkeySequenceOptions
+}
+
+/**
+ * SolidJS primitive for registering multiple keyboard shortcut sequences at once (Vim-style).
+ *
+ * Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or accessors,
+ * so you can react to variable-length lists.
+ *
+ * Options are merged in this order:
+ * HotkeysProvider defaults < commonOptions < per-definition options
+ *
+ * Definitions with an empty `sequence` are skipped (no registration).
+ *
+ * @param sequences - Array of sequence definitions, or accessor returning them
+ * @param commonOptions - Shared options for all sequences, or accessor
+ *
+ * @example
+ * ```tsx
+ * function VimPalette() {
+ * createHotkeySequences([
+ * { sequence: ['G', 'G'], callback: () => scrollToTop() },
+ * { sequence: ['D', 'D'], callback: () => deleteLine() },
+ * ])
+ * }
+ * ```
+ *
+ * @example
+ * ```tsx
+ * function Dynamic(props) {
+ * createHotkeySequences(
+ * () => props.items.map((item) => ({
+ * sequence: item.chords,
+ * callback: item.action,
+ * options: { enabled: item.enabled },
+ * })),
+ * { preventDefault: true },
+ * )
+ * }
+ * ```
+ */
+export function createHotkeySequences(
+ sequences:
+ | Array
+ | (() => Array),
+ commonOptions:
+ | CreateHotkeySequenceOptions
+ | (() => CreateHotkeySequenceOptions) = {},
+): void {
+ type RegistrationRecord = {
+ handle: SequenceRegistrationHandle
+ target: Document | HTMLElement | Window
+ }
+
+ const defaultOptions = useDefaultHotkeysOptions()
+ const manager = getSequenceManager()
+
+ const registrations = new Map()
+
+ onCleanup(() => {
+ for (const { handle } of registrations.values()) {
+ if (handle.isActive) {
+ handle.unregister()
+ }
+ }
+ registrations.clear()
+ })
+
+ createEffect(() => {
+ const resolved = typeof sequences === 'function' ? sequences() : sequences
+ const resolvedCommonOptions =
+ typeof commonOptions === 'function' ? commonOptions() : commonOptions
+
+ const nextKeys = new Set()
+ const prepared: Array<{
+ registrationKey: string
+ def: CreateHotkeySequenceDefinition
+ mergedOptions: CreateHotkeySequenceOptions
+ sequenceString: string
+ resolvedTarget: Document | HTMLElement | Window
+ }> = []
+
+ for (let i = 0; i < resolved.length; i++) {
+ const def = resolved[i]!
+ const resolvedDefOptions = def.options ?? {}
+
+ const mergedOptions = {
+ ...defaultOptions.hotkeySequence,
+ ...resolvedCommonOptions,
+ ...resolvedDefOptions,
+ } as CreateHotkeySequenceOptions
+
+ if (def.sequence.length === 0) {
+ continue
+ }
+
+ const sequenceString = formatHotkeySequence(def.sequence)
+
+ const resolvedTarget =
+ 'target' in mergedOptions
+ ? (mergedOptions.target ?? null)
+ : typeof document !== 'undefined'
+ ? document
+ : null
+
+ if (!resolvedTarget) {
+ continue
+ }
+
+ const registrationKey = `${i}:${sequenceString}`
+ nextKeys.add(registrationKey)
+ prepared.push({
+ registrationKey,
+ def,
+ mergedOptions,
+ sequenceString,
+ resolvedTarget,
+ })
+ }
+
+ for (const [key, record] of [...registrations.entries()]) {
+ if (!nextKeys.has(key)) {
+ if (record.handle.isActive) {
+ record.handle.unregister()
+ }
+ registrations.delete(key)
+ }
+ }
+
+ for (const p of prepared) {
+ const existing = registrations.get(p.registrationKey)
+ if (existing?.handle.isActive && existing.target === p.resolvedTarget) {
+ existing.handle.callback = p.def.callback
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions
+ existing.handle.setOptions(optionsWithoutTarget)
+ continue
+ }
+
+ if (existing) {
+ if (existing.handle.isActive) {
+ existing.handle.unregister()
+ }
+ registrations.delete(p.registrationKey)
+ }
+
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions
+ const handle = manager.register(p.def.sequence, p.def.callback, {
+ ...optionsWithoutTarget,
+ target: p.resolvedTarget,
+ })
+ registrations.set(p.registrationKey, {
+ handle,
+ target: p.resolvedTarget,
+ })
+ }
+ })
+}
diff --git a/packages/solid-hotkeys/src/index.ts b/packages/solid-hotkeys/src/index.ts
index 5c079982..9aa45f70 100644
--- a/packages/solid-hotkeys/src/index.ts
+++ b/packages/solid-hotkeys/src/index.ts
@@ -11,5 +11,6 @@ export * from './createHeldKeys'
export * from './createHeldKeyCodes'
export * from './createKeyHold'
export * from './createHotkeySequence'
+export * from './createHotkeySequences'
export * from './createHotkeyRecorder'
export * from './createHotkeySequenceRecorder'
diff --git a/packages/solid-hotkeys/tests/createHotkeySequences.test.tsx b/packages/solid-hotkeys/tests/createHotkeySequences.test.tsx
new file mode 100644
index 00000000..49897cff
--- /dev/null
+++ b/packages/solid-hotkeys/tests/createHotkeySequences.test.tsx
@@ -0,0 +1,188 @@
+// @vitest-environment happy-dom
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { render } from '@solidjs/testing-library'
+import { SequenceManager } from '@tanstack/hotkeys'
+import { createSignal } from 'solid-js'
+import type { Component } from 'solid-js'
+import { createHotkeySequences } from '../src/createHotkeySequences'
+import type { CreateHotkeySequenceDefinition } from '../src/createHotkeySequences'
+
+function dispatchKey(key: string) {
+ document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
+}
+
+describe('createHotkeySequences', () => {
+ beforeEach(() => {
+ SequenceManager.resetInstance()
+ })
+
+ afterEach(() => {
+ SequenceManager.resetInstance()
+ })
+
+ it('should register multiple sequence handlers', () => {
+ const a = vi.fn()
+ const b = vi.fn()
+
+ const TestComponent: Component = () => {
+ createHotkeySequences([
+ { sequence: ['G', 'G'], callback: a },
+ { sequence: ['D', 'D'], callback: b },
+ ])
+ return null
+ }
+
+ render(() => )
+
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(2)
+ })
+
+ it('should call the correct callback for each sequence', () => {
+ const gg = vi.fn()
+ const dd = vi.fn()
+
+ const TestComponent: Component = () => {
+ createHotkeySequences([
+ { sequence: ['G', 'G'], callback: gg },
+ { sequence: ['D', 'D'], callback: dd },
+ ])
+ return null
+ }
+
+ render(() => )
+
+ dispatchKey('g')
+ dispatchKey('g')
+ expect(gg).toHaveBeenCalledTimes(1)
+ expect(dd).not.toHaveBeenCalled()
+
+ dispatchKey('d')
+ dispatchKey('d')
+ expect(gg).toHaveBeenCalledTimes(1)
+ expect(dd).toHaveBeenCalledTimes(1)
+ })
+
+ it('should unregister all sequences on unmount', () => {
+ const TestComponent: Component = () => {
+ createHotkeySequences([
+ { sequence: ['G', 'G'], callback: vi.fn() },
+ { sequence: ['D', 'D'], callback: vi.fn() },
+ ])
+ return null
+ }
+
+ const { unmount } = render(() => )
+
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(2)
+ unmount()
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(0)
+ })
+
+ it('should handle an empty array as a no-op', () => {
+ const TestComponent: Component = () => {
+ createHotkeySequences([])
+ return null
+ }
+
+ render(() => )
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(0)
+ })
+
+ it('should skip definitions with an empty sequence', () => {
+ const TestComponent: Component = () => {
+ createHotkeySequences([
+ { sequence: [], callback: vi.fn() },
+ { sequence: ['G', 'G'], callback: vi.fn() },
+ ])
+ return null
+ }
+
+ render(() => )
+ expect(SequenceManager.getInstance().getRegistrationCount()).toBe(1)
+ })
+
+ it('should register disabled sequences and keep them in the manager', () => {
+ const enabledCb = vi.fn()
+ const disabledCb = vi.fn()
+
+ const TestComponent: Component = () => {
+ createHotkeySequences([
+ { sequence: ['G', 'G'], callback: enabledCb },
+ {
+ sequence: ['D', 'D'],
+ callback: disabledCb,
+ options: { enabled: false },
+ },
+ ])
+ return null
+ }
+
+ render(() => )
+
+ dispatchKey('g')
+ dispatchKey('g')
+ dispatchKey('d')
+ dispatchKey('d')
+ expect(enabledCb).toHaveBeenCalledTimes(1)
+ expect(disabledCb).not.toHaveBeenCalled()
+
+ const manager = SequenceManager.getInstance()
+ expect(manager.getRegistrationCount()).toBe(2)
+ const disabledView = [...manager.registrations.state.values()].find(
+ (r) => r.sequence[0] === 'D' && r.sequence[1] === 'D',
+ )
+ expect(disabledView?.options.enabled).toBe(false)
+ })
+
+ it('should preserve registration id when toggling enabled', () => {
+ const callback = vi.fn()
+ const manager = SequenceManager.getInstance()
+ const [enabled, setEnabled] = createSignal(true)
+
+ const TestComponent: Component = () => {
+ createHotkeySequences(() => [
+ { sequence: ['G', 'G'], callback, options: { enabled: enabled() } },
+ ])
+ return null
+ }
+
+ render(() => )
+
+ const idBefore = [...manager.registrations.state.keys()][0]
+ expect(manager.getRegistrationCount()).toBe(1)
+
+ setEnabled(false)
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
+
+ setEnabled(true)
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
+ })
+
+ it('should handle dynamic accessor for definitions', () => {
+ const gg = vi.fn()
+ const yy = vi.fn()
+ const [defs, setDefs] = createSignal>(
+ [{ sequence: ['G', 'G'], callback: gg }],
+ )
+
+ const TestComponent: Component = () => {
+ createHotkeySequences(() => defs())
+ return null
+ }
+
+ render(() => )
+
+ dispatchKey('y')
+ dispatchKey('y')
+ expect(yy).not.toHaveBeenCalled()
+
+ setDefs([
+ { sequence: ['G', 'G'], callback: gg },
+ { sequence: ['Y', 'Y'], callback: yy },
+ ])
+
+ dispatchKey('y')
+ dispatchKey('y')
+ expect(yy).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/packages/solid-hotkeys/tests/createHotkeys.test.tsx b/packages/solid-hotkeys/tests/createHotkeys.test.tsx
index e95af02e..cd8bb0b3 100644
--- a/packages/solid-hotkeys/tests/createHotkeys.test.tsx
+++ b/packages/solid-hotkeys/tests/createHotkeys.test.tsx
@@ -146,6 +146,41 @@ describe('createHotkeys', () => {
expect(enabledCb).toHaveBeenCalledTimes(1)
expect(disabledCb).not.toHaveBeenCalled()
+
+ const manager = HotkeyManager.getInstance()
+ expect(manager.getRegistrationCount()).toBe(2)
+ const disabledReg = [...manager.registrations.state.values()].find(
+ (r) => r.hotkey === 'Mod+Z',
+ )
+ expect(disabledReg?.options.enabled).toBe(false)
+ })
+
+ it('should preserve registration id when toggling enabled', () => {
+ const callback = vi.fn()
+ const manager = HotkeyManager.getInstance()
+ const [enabled, setEnabled] = createSignal(true)
+
+ const TestComponent: Component = () => {
+ createHotkeys(() => [
+ {
+ hotkey: 'Mod+S',
+ callback,
+ options: { platform: 'mac', enabled: enabled() },
+ },
+ ])
+ return null
+ }
+
+ render(() => )
+
+ const idBefore = [...manager.registrations.state.keys()][0]
+ expect(manager.getRegistrationCount()).toBe(1)
+
+ setEnabled(false)
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
+
+ setEnabled(true)
+ expect([...manager.registrations.state.keys()][0]).toBe(idBefore)
})
it('should handle dynamic array changes via accessor', () => {
diff --git a/packages/svelte-hotkeys/src/createHotkey.svelte.ts b/packages/svelte-hotkeys/src/createHotkey.svelte.ts
index 6a15b1a0..a5acbee1 100644
--- a/packages/svelte-hotkeys/src/createHotkey.svelte.ts
+++ b/packages/svelte-hotkeys/src/createHotkey.svelte.ts
@@ -4,12 +4,14 @@ import {
getHotkeyManager,
rawHotkeyToParsedHotkey,
} from '@tanstack/hotkeys'
+import { onDestroy } from 'svelte'
import { getDefaultHotkeysOptions } from './HotkeysCtx'
import { resolveMaybeGetter } from './internal.svelte'
import type {
Hotkey,
HotkeyCallback,
HotkeyOptions,
+ HotkeyRegistrationHandle,
RegisterableHotkey,
} from '@tanstack/hotkeys'
import type { MaybeGetter } from './internal.svelte'
@@ -32,19 +34,12 @@ function normalizeHotkey(
function registerHotkey(
target: HTMLElement | Document | Window,
- hotkey: MaybeGetter,
+ hotkey: RegisterableHotkey,
callback: HotkeyCallback,
- options: MaybeGetter,
+ mergedOptions: CreateHotkeyOptions,
) {
- const resolvedHotkey = resolveMaybeGetter(hotkey)
- const resolvedOptions = resolveMaybeGetter(options)
- const mergedOptions = {
- ...getDefaultHotkeysOptions().hotkey,
- ...resolvedOptions,
- } as CreateHotkeyOptions
-
return getHotkeyManager().register(
- normalizeHotkey(resolvedHotkey, mergedOptions),
+ normalizeHotkey(hotkey, mergedOptions),
callback,
{
...mergedOptions,
@@ -72,15 +67,48 @@ export function createHotkey(
callback: HotkeyCallback,
options: MaybeGetter = {},
): void {
+ let registration: HotkeyRegistrationHandle | null = null
+ let lastHotkeyStr: Hotkey | null = null
+
$effect(() => {
if (typeof document === 'undefined') {
return
}
- const registration = registerHotkey(document, hotkey, callback, options)
+ const resolvedHotkey = resolveMaybeGetter(hotkey)
+ const resolvedOptions = resolveMaybeGetter(options)
+ const mergedOptions = {
+ ...getDefaultHotkeysOptions().hotkey,
+ ...resolvedOptions,
+ } as CreateHotkeyOptions
- return () => {
+ const hotkeyStr = normalizeHotkey(resolvedHotkey, mergedOptions)
+ const { target: _t, ...optionsWithoutTarget } = mergedOptions
+
+ if (registration?.isActive && lastHotkeyStr === hotkeyStr) {
+ registration.callback = callback
+ registration.setOptions(optionsWithoutTarget)
+ return
+ }
+
+ if (registration?.isActive) {
registration.unregister()
+ registration = null
+ }
+
+ registration = registerHotkey(
+ document,
+ resolvedHotkey,
+ callback,
+ mergedOptions,
+ )
+ lastHotkeyStr = hotkeyStr
+ })
+
+ onDestroy(() => {
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
}
})
}
@@ -111,14 +139,38 @@ export function createHotkeyAttachment(
options: MaybeGetter = {},
): Attachment {
return (element) => {
- let registration: ReturnType | null = null
+ let registration: HotkeyRegistrationHandle | null = null
+ let lastHotkeyStr: Hotkey | null = null
$effect(() => {
+ const resolvedHotkey = resolveMaybeGetter(hotkey)
+ const resolvedOptions = resolveMaybeGetter(options)
+ const mergedOptions = {
+ ...getDefaultHotkeysOptions().hotkey,
+ ...resolvedOptions,
+ } as CreateHotkeyOptions
+
+ const hotkeyStr = normalizeHotkey(resolvedHotkey, mergedOptions)
+ const { target: _t, ...optionsWithoutTarget } = mergedOptions
+
+ if (registration?.isActive && lastHotkeyStr === hotkeyStr) {
+ registration.callback = callback
+ registration.setOptions(optionsWithoutTarget)
+ return
+ }
+
if (registration?.isActive) {
registration.unregister()
+ registration = null
}
- registration = registerHotkey(element, hotkey, callback, options)
+ registration = registerHotkey(
+ element,
+ resolvedHotkey,
+ callback,
+ mergedOptions,
+ )
+ lastHotkeyStr = hotkeyStr
})
return () => {
diff --git a/packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts b/packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts
index 21d8cff3..e740d6b0 100644
--- a/packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts
+++ b/packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts
@@ -1,10 +1,12 @@
-import { getSequenceManager } from '@tanstack/hotkeys'
+import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
+import { onDestroy } from 'svelte'
import { getDefaultHotkeysOptions } from './HotkeysCtx'
import { resolveMaybeGetter } from './internal.svelte'
import type {
HotkeyCallback,
HotkeySequence,
SequenceOptions,
+ SequenceRegistrationHandle,
} from '@tanstack/hotkeys'
import type { MaybeGetter } from './internal.svelte'
import type { Attachment } from 'svelte/attachments'
@@ -18,18 +20,11 @@ export interface CreateHotkeySequenceOptions extends Omit<
function registerHotkeySequence(
target: HTMLElement | Document | Window,
- sequence: MaybeGetter,
+ sequence: HotkeySequence,
callback: HotkeyCallback,
- options: MaybeGetter,
+ mergedOptions: CreateHotkeySequenceOptions,
) {
- const resolvedSequence = resolveMaybeGetter(sequence)
- const resolvedOptions = resolveMaybeGetter(options)
- const mergedOptions = {
- ...getDefaultHotkeysOptions().hotkeySequence,
- ...resolvedOptions,
- } as CreateHotkeySequenceOptions
-
- return getSequenceManager().register(resolvedSequence, callback, {
+ return getSequenceManager().register(sequence, callback, {
...mergedOptions,
target,
})
@@ -79,25 +74,58 @@ export function createHotkeySequence(
callback: HotkeyCallback,
options: MaybeGetter = {},
): void {
+ let registration: SequenceRegistrationHandle | null = null
+ let lastSequenceKey: string | null = null
+
$effect(() => {
if (typeof document === 'undefined') {
return
}
const resolvedSequence = resolveMaybeGetter(sequence)
+ const resolvedOptions = resolveMaybeGetter(options)
+ const mergedOptions = {
+ ...getDefaultHotkeysOptions().hotkeySequence,
+ ...resolvedOptions,
+ } as CreateHotkeySequenceOptions
+
+ const { target: _t, ...optionsWithoutTarget } = mergedOptions
+
if (resolvedSequence.length === 0) {
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
+ }
+ lastSequenceKey = null
return
}
- const registration = registerHotkeySequence(
+ const sequenceKey = formatHotkeySequence(resolvedSequence)
+
+ if (registration?.isActive && lastSequenceKey === sequenceKey) {
+ registration.callback = callback
+ registration.setOptions(optionsWithoutTarget)
+ return
+ }
+
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
+ }
+
+ registration = registerHotkeySequence(
document,
resolvedSequence,
callback,
- options,
+ mergedOptions,
)
+ lastSequenceKey = sequenceKey
+ })
- return () => {
+ onDestroy(() => {
+ if (registration?.isActive) {
registration.unregister()
+ registration = null
}
})
}
@@ -126,26 +154,48 @@ export function createHotkeySequenceAttachment(
options: MaybeGetter = {},
): Attachment {
return (element) => {
- let registration: ReturnType | null = null
+ let registration: SequenceRegistrationHandle | null = null
+ let lastSequenceKey: string | null = null
$effect(() => {
const resolvedSequence = resolveMaybeGetter(sequence)
+ const resolvedOptions = resolveMaybeGetter(options)
+ const mergedOptions = {
+ ...getDefaultHotkeysOptions().hotkeySequence,
+ ...resolvedOptions,
+ } as CreateHotkeySequenceOptions
- if (registration?.isActive) {
- registration.unregister()
- registration = null
- }
+ const { target: _t, ...optionsWithoutTarget } = mergedOptions
if (resolvedSequence.length === 0) {
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
+ }
+ lastSequenceKey = null
return
}
+ const sequenceKey = formatHotkeySequence(resolvedSequence)
+
+ if (registration?.isActive && lastSequenceKey === sequenceKey) {
+ registration.callback = callback
+ registration.setOptions(optionsWithoutTarget)
+ return
+ }
+
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
+ }
+
registration = registerHotkeySequence(
element,
resolvedSequence,
callback,
- options,
+ mergedOptions,
)
+ lastSequenceKey = sequenceKey
})
return () => {
diff --git a/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts b/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts
new file mode 100644
index 00000000..2b2b849c
--- /dev/null
+++ b/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts
@@ -0,0 +1,274 @@
+import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
+import { onDestroy } from 'svelte'
+import { SvelteMap, SvelteSet } from 'svelte/reactivity'
+import { getDefaultHotkeysOptions } from './HotkeysCtx'
+import { resolveMaybeGetter } from './internal.svelte'
+import type {
+ HotkeyCallback,
+ HotkeySequence,
+ SequenceRegistrationHandle,
+} from '@tanstack/hotkeys'
+import type { CreateHotkeySequenceOptions } from './createHotkeySequence.svelte'
+import type { MaybeGetter } from './internal.svelte'
+import type { Attachment } from 'svelte/attachments'
+
+/**
+ * A single sequence definition for use with `createHotkeySequences`.
+ */
+export interface CreateHotkeySequenceDefinition {
+ /** Array of hotkey strings that form the sequence */
+ sequence: MaybeGetter
+ /** The function to call when the sequence is completed */
+ callback: HotkeyCallback
+ /** Per-sequence options (merged on top of commonOptions) */
+ options?: MaybeGetter
+}
+
+type RegistrationRecord = {
+ handle: SequenceRegistrationHandle
+ target: Document | HTMLElement | Window
+}
+
+function cleanupRegistrations(
+ registrations:
+ | Map
+ | SvelteMap,
+) {
+ for (const { handle } of registrations.values()) {
+ if (handle.isActive) {
+ handle.unregister()
+ }
+ }
+ registrations.clear()
+}
+
+/**
+ * Register multiple global keyboard shortcut sequences for the current component.
+ *
+ * @example
+ * ```svelte
+ *
+ * ```
+ */
+export function createHotkeySequences(
+ definitions: MaybeGetter>,
+ commonOptions: MaybeGetter = {},
+): void {
+ const registrations = new SvelteMap()
+
+ onDestroy(() => {
+ cleanupRegistrations(registrations)
+ })
+
+ $effect(() => {
+ if (typeof document === 'undefined') {
+ return
+ }
+
+ const resolvedDefinitions = resolveMaybeGetter(definitions)
+ const resolvedCommonOptions = resolveMaybeGetter(commonOptions)
+ const nextKeys = new SvelteSet()
+ const prepared: Array<{
+ registrationKey: string
+ def: CreateHotkeySequenceDefinition
+ mergedOptions: CreateHotkeySequenceOptions
+ sequenceString: string
+ resolvedSequence: HotkeySequence
+ resolvedTarget: Document | HTMLElement | Window
+ }> = []
+
+ for (let i = 0; i < resolvedDefinitions.length; i++) {
+ const def = resolvedDefinitions[i]!
+ const resolvedSequence = resolveMaybeGetter(def.sequence)
+ if (resolvedSequence.length === 0) {
+ continue
+ }
+
+ const resolvedDefOptions = def.options
+ ? resolveMaybeGetter(def.options)
+ : {}
+
+ const mergedOptions = {
+ ...getDefaultHotkeysOptions().hotkeySequence,
+ ...resolvedCommonOptions,
+ ...resolvedDefOptions,
+ } as CreateHotkeySequenceOptions
+
+ const resolvedTarget =
+ mergedOptions.target ??
+ (typeof document !== 'undefined' ? document : null)
+
+ if (!resolvedTarget) {
+ continue
+ }
+
+ const sequenceString = formatHotkeySequence(resolvedSequence)
+ const registrationKey = `${i}:${sequenceString}`
+ nextKeys.add(registrationKey)
+ prepared.push({
+ registrationKey,
+ def,
+ mergedOptions,
+ sequenceString,
+ resolvedSequence,
+ resolvedTarget,
+ })
+ }
+
+ for (const [key, record] of [...registrations.entries()]) {
+ if (!nextKeys.has(key)) {
+ if (record.handle.isActive) {
+ record.handle.unregister()
+ }
+ registrations.delete(key)
+ }
+ }
+
+ for (const p of prepared) {
+ const existing = registrations.get(p.registrationKey)
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions
+
+ if (existing?.handle.isActive && existing.target === p.resolvedTarget) {
+ existing.handle.callback = p.def.callback
+ existing.handle.setOptions(optionsWithoutTarget)
+ continue
+ }
+
+ if (existing?.handle.isActive) {
+ existing.handle.unregister()
+ }
+ if (existing) {
+ registrations.delete(p.registrationKey)
+ }
+
+ const handle = getSequenceManager().register(
+ p.resolvedSequence,
+ p.def.callback,
+ {
+ ...optionsWithoutTarget,
+ target: p.resolvedTarget,
+ },
+ )
+ registrations.set(p.registrationKey, {
+ handle,
+ target: p.resolvedTarget,
+ })
+ }
+ })
+}
+
+/**
+ * Create an attachment for element-scoped multi-sequence registration.
+ *
+ * @example
+ * ```svelte
+ *
+ *
+ * Editor
+ * ```
+ */
+export function createHotkeySequencesAttachment(
+ definitions: MaybeGetter>,
+ commonOptions: MaybeGetter = {},
+): Attachment {
+ return (element) => {
+ const registrations = new SvelteMap()
+
+ $effect(() => {
+ const resolvedDefinitions = resolveMaybeGetter(definitions)
+ const resolvedCommonOptions = resolveMaybeGetter(commonOptions)
+ const nextKeys = new SvelteSet()
+ const prepared: Array<{
+ registrationKey: string
+ def: CreateHotkeySequenceDefinition
+ mergedOptions: CreateHotkeySequenceOptions
+ sequenceString: string
+ resolvedSequence: HotkeySequence
+ }> = []
+
+ for (let i = 0; i < resolvedDefinitions.length; i++) {
+ const def = resolvedDefinitions[i]!
+ const resolvedSequence = resolveMaybeGetter(def.sequence)
+ if (resolvedSequence.length === 0) {
+ continue
+ }
+
+ const resolvedDefOptions = def.options
+ ? resolveMaybeGetter(def.options)
+ : {}
+
+ const mergedOptions = {
+ ...getDefaultHotkeysOptions().hotkeySequence,
+ ...resolvedCommonOptions,
+ ...resolvedDefOptions,
+ } as CreateHotkeySequenceOptions
+
+ const sequenceString = formatHotkeySequence(resolvedSequence)
+ const registrationKey = `${i}:${sequenceString}`
+ nextKeys.add(registrationKey)
+ prepared.push({
+ registrationKey,
+ def,
+ mergedOptions,
+ sequenceString,
+ resolvedSequence,
+ })
+ }
+
+ for (const [key, record] of [...registrations.entries()]) {
+ if (!nextKeys.has(key)) {
+ if (record.handle.isActive) {
+ record.handle.unregister()
+ }
+ registrations.delete(key)
+ }
+ }
+
+ for (const p of prepared) {
+ const existing = registrations.get(p.registrationKey)
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions
+
+ if (existing?.handle.isActive && existing.target === element) {
+ existing.handle.callback = p.def.callback
+ existing.handle.setOptions(optionsWithoutTarget)
+ continue
+ }
+
+ if (existing?.handle.isActive) {
+ existing.handle.unregister()
+ }
+ if (existing) {
+ registrations.delete(p.registrationKey)
+ }
+
+ const handle = getSequenceManager().register(
+ p.resolvedSequence,
+ p.def.callback,
+ {
+ ...optionsWithoutTarget,
+ target: element,
+ },
+ )
+ registrations.set(p.registrationKey, { handle, target: element })
+ }
+ })
+
+ return () => {
+ cleanupRegistrations(registrations)
+ }
+ }
+}
diff --git a/packages/svelte-hotkeys/src/index.ts b/packages/svelte-hotkeys/src/index.ts
index b5ce4329..93b44be4 100644
--- a/packages/svelte-hotkeys/src/index.ts
+++ b/packages/svelte-hotkeys/src/index.ts
@@ -4,6 +4,7 @@ export * from '@tanstack/hotkeys'
export * from './createHotkey.svelte'
export * from './createHotkeys.svelte'
export * from './createHotkeySequence.svelte'
+export * from './createHotkeySequences.svelte'
export * from './createHotkeyRecorder.svelte'
export * from './createHotkeySequenceRecorder.svelte'
export * from './getHeldKeys.svelte'
diff --git a/packages/vue-hotkeys/src/index.ts b/packages/vue-hotkeys/src/index.ts
index faf548e8..37a24c14 100644
--- a/packages/vue-hotkeys/src/index.ts
+++ b/packages/vue-hotkeys/src/index.ts
@@ -12,5 +12,6 @@ export * from './useHeldKeys'
export * from './useHeldKeyCodes'
export * from './useKeyHold'
export * from './useHotkeySequence'
+export * from './useHotkeySequences'
export * from './useHotkeyRecorder'
export * from './useHotkeySequenceRecorder'
diff --git a/packages/vue-hotkeys/src/useHotkey.ts b/packages/vue-hotkeys/src/useHotkey.ts
index 58c3acb4..8701654d 100644
--- a/packages/vue-hotkeys/src/useHotkey.ts
+++ b/packages/vue-hotkeys/src/useHotkey.ts
@@ -106,6 +106,8 @@ export function useHotkey(
const manager = getHotkeyManager()
let registration: HotkeyRegistrationHandle | null = null
+ let lastHotkeyString: Hotkey | null = null
+ let lastTarget: HTMLElement | Document | Window | null = null
// Watch for changes to reactive dependencies
const stopWatcher = watch(
@@ -144,18 +146,22 @@ export function useHotkey(
// Resolve target
const finalTarget =
- resolvedTarget ?? (typeof document !== 'undefined' ? document : null)
+ resolvedTarget === undefined
+ ? typeof document !== 'undefined'
+ ? document
+ : null
+ : resolvedTarget
if (!finalTarget) {
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
+ }
+ lastHotkeyString = null
+ lastTarget = null
return
}
- // Unregister previous registration if it exists
- if (registration?.isActive) {
- registration.unregister()
- registration = null
- }
-
// Extract options without target (target is handled separately)
const {
target: _target,
@@ -167,17 +173,33 @@ export function useHotkey(
...(resolvedEnabled === undefined ? {} : { enabled: resolvedEnabled }),
}
- // Register the hotkey
+ if (
+ registration?.isActive &&
+ lastHotkeyString === hotkeyString &&
+ lastTarget === finalTarget
+ ) {
+ registration.callback = callback
+ registration.setOptions(optionsWithoutTarget)
+ return
+ }
+
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
+ }
+
registration = manager.register(hotkeyString, callback, {
...optionsWithoutTarget,
target: finalTarget,
})
- // Update callback and options
if (registration.isActive) {
registration.callback = callback
registration.setOptions(optionsWithoutTarget)
}
+
+ lastHotkeyString = hotkeyString
+ lastTarget = finalTarget
},
{ immediate: true },
)
diff --git a/packages/vue-hotkeys/src/useHotkeySequence.ts b/packages/vue-hotkeys/src/useHotkeySequence.ts
index a1c89864..21f13575 100644
--- a/packages/vue-hotkeys/src/useHotkeySequence.ts
+++ b/packages/vue-hotkeys/src/useHotkeySequence.ts
@@ -1,5 +1,5 @@
import { onUnmounted, unref, watch } from 'vue'
-import { getSequenceManager } from '@tanstack/hotkeys'
+import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
import { useDefaultHotkeysOptions } from './HotkeysProviderContext'
import type { MaybeRefOrGetter } from 'vue'
import type {
@@ -82,6 +82,8 @@ export function useHotkeySequence(
const manager = getSequenceManager()
let registration: SequenceRegistrationHandle | null = null
+ let lastSequenceKey: string | null = null
+ let lastTarget: HTMLElement | Document | Window | null = null
// Watch for changes to reactive dependencies
const stopWatcher = watch(
@@ -109,23 +111,24 @@ export function useHotkeySequence(
}
},
({ resolvedSequence, mergedOptions, resolvedEnabled, resolvedTarget }) => {
- if (resolvedEnabled === false || resolvedSequence.length === 0) {
- return
- }
-
- // Resolve target
const finalTarget =
- resolvedTarget ?? (typeof document !== 'undefined' ? document : null)
+ resolvedTarget === undefined
+ ? typeof document !== 'undefined'
+ ? document
+ : null
+ : resolvedTarget
- if (!finalTarget) {
+ if (resolvedSequence.length === 0 || !finalTarget) {
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
+ }
+ lastSequenceKey = null
+ lastTarget = null
return
}
- // Unregister previous registration if it exists
- if (registration?.isActive) {
- registration.unregister()
- registration = null
- }
+ const sequenceKey = formatHotkeySequence(resolvedSequence)
// Extract options without target (target is handled separately)
const {
@@ -138,17 +141,33 @@ export function useHotkeySequence(
...(resolvedEnabled === undefined ? {} : { enabled: resolvedEnabled }),
}
- // Register the sequence
+ if (
+ registration?.isActive &&
+ lastSequenceKey === sequenceKey &&
+ lastTarget === finalTarget
+ ) {
+ registration.callback = callback
+ registration.setOptions(optionsWithoutTarget)
+ return
+ }
+
+ if (registration?.isActive) {
+ registration.unregister()
+ registration = null
+ }
+
registration = manager.register(resolvedSequence, callback, {
...optionsWithoutTarget,
target: finalTarget,
})
- // Update callback and options
if (registration.isActive) {
registration.callback = callback
registration.setOptions(optionsWithoutTarget)
}
+
+ lastSequenceKey = sequenceKey
+ lastTarget = finalTarget
},
{ immediate: true },
)
diff --git a/packages/vue-hotkeys/src/useHotkeySequences.ts b/packages/vue-hotkeys/src/useHotkeySequences.ts
new file mode 100644
index 00000000..4c746f50
--- /dev/null
+++ b/packages/vue-hotkeys/src/useHotkeySequences.ts
@@ -0,0 +1,221 @@
+import { onUnmounted, unref, watch } from 'vue'
+import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
+import { useDefaultHotkeysOptions } from './HotkeysProviderContext'
+import type { UseHotkeySequenceOptions } from './useHotkeySequence'
+import type {
+ HotkeyCallback,
+ HotkeySequence,
+ SequenceRegistrationHandle,
+} from '@tanstack/hotkeys'
+import type { MaybeRefOrGetter } from 'vue'
+
+/**
+ * A single sequence definition for use with `useHotkeySequences`.
+ */
+export interface UseHotkeySequenceDefinition {
+ /** Array of hotkey strings that form the sequence */
+ sequence: MaybeRefOrGetter
+ /** The function to call when the sequence is completed */
+ callback: HotkeyCallback
+ /** Per-sequence options (merged on top of commonOptions) */
+ options?: MaybeRefOrGetter
+}
+
+/**
+ * Vue composable for registering multiple keyboard shortcut sequences at once (Vim-style).
+ *
+ * Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or a getter/ref
+ * that returns one, so you can register variable-length lists safely.
+ *
+ * Options are merged in this order:
+ * HotkeysProvider defaults < commonOptions < per-definition options
+ *
+ * Definitions with an empty `sequence` are skipped (no registration).
+ *
+ * @param definitions - Array of sequence definitions, or a getter/ref
+ * @param commonOptions - Shared options applied to all sequences (overridden by per-definition options)
+ *
+ * @example
+ * ```vue
+ *
+ * ```
+ *
+ * @example
+ * ```vue
+ *
+ * ```
+ */
+export function useHotkeySequences(
+ definitions: MaybeRefOrGetter>,
+ commonOptions: MaybeRefOrGetter = {},
+): void {
+ type RegistrationRecord = {
+ handle: SequenceRegistrationHandle
+ target: Document | HTMLElement | Window
+ }
+
+ const defaultOptions = useDefaultHotkeysOptions()
+ const manager = getSequenceManager()
+
+ const registrations = new Map()
+
+ const stopWatcher = watch(
+ () => {
+ const resolvedDefinitions = resolveMaybeRefOrGetter(definitions)
+ const resolvedCommonOptions = resolveMaybeRefOrGetter(commonOptions)
+
+ return resolvedDefinitions.map((def, i) => {
+ const resolvedSequence = resolveMaybeRefOrGetter(def.sequence)
+ const resolvedDefOptions = def.options
+ ? resolveMaybeRefOrGetter(def.options)
+ : {}
+
+ const mergedOptions = {
+ ...defaultOptions.hotkeySequence,
+ ...resolvedCommonOptions,
+ ...resolvedDefOptions,
+ } as UseHotkeySequenceOptions
+
+ const sequenceString = formatHotkeySequence(resolvedSequence)
+
+ const resolvedEnabled =
+ mergedOptions.enabled === undefined
+ ? undefined
+ : resolveMaybeRefOrGetter(mergedOptions.enabled)
+ const resolvedTarget =
+ mergedOptions.target === undefined
+ ? undefined
+ : resolveMaybeRefOrGetter(mergedOptions.target as any)
+
+ return {
+ index: i,
+ resolvedSequence,
+ sequenceString,
+ callback: def.callback,
+ mergedOptions,
+ resolvedEnabled,
+ resolvedTarget,
+ }
+ })
+ },
+ (resolved) => {
+ const nextKeys = new Set()
+ const prepared: Array<{
+ registrationKey: string
+ entry: (typeof resolved)[number]
+ finalTarget: Document | HTMLElement | Window
+ }> = []
+
+ for (const entry of resolved) {
+ if (entry.resolvedSequence.length === 0) {
+ continue
+ }
+
+ const finalTarget =
+ entry.resolvedTarget ??
+ (typeof document !== 'undefined' ? document : null)
+
+ if (!finalTarget) {
+ continue
+ }
+
+ const registrationKey = `${entry.index}:${entry.sequenceString}`
+ nextKeys.add(registrationKey)
+ prepared.push({ registrationKey, entry, finalTarget })
+ }
+
+ for (const [key, record] of [...registrations.entries()]) {
+ if (!nextKeys.has(key)) {
+ if (record.handle.isActive) {
+ record.handle.unregister()
+ }
+ registrations.delete(key)
+ }
+ }
+
+ for (const { registrationKey, entry, finalTarget } of prepared) {
+ const existing = registrations.get(registrationKey)
+ if (existing?.handle.isActive && existing.target === finalTarget) {
+ existing.handle.callback = entry.callback
+ const {
+ target: _target,
+ enabled: _enabled,
+ ...restOptions
+ } = entry.mergedOptions
+ const optionsWithoutTarget = {
+ ...restOptions,
+ ...(entry.resolvedEnabled === undefined
+ ? {}
+ : { enabled: entry.resolvedEnabled }),
+ }
+ existing.handle.setOptions(optionsWithoutTarget)
+ continue
+ }
+
+ if (existing) {
+ if (existing.handle.isActive) {
+ existing.handle.unregister()
+ }
+ registrations.delete(registrationKey)
+ }
+
+ const {
+ target: _target,
+ enabled: _enabled,
+ ...restOptions
+ } = entry.mergedOptions
+ const optionsWithoutTarget = {
+ ...restOptions,
+ ...(entry.resolvedEnabled === undefined
+ ? {}
+ : { enabled: entry.resolvedEnabled }),
+ }
+
+ const handle = manager.register(
+ entry.resolvedSequence,
+ entry.callback,
+ {
+ ...optionsWithoutTarget,
+ target: finalTarget,
+ },
+ )
+ registrations.set(registrationKey, { handle, target: finalTarget })
+ }
+ },
+ { immediate: true },
+ )
+
+ onUnmounted(() => {
+ stopWatcher()
+ for (const { handle } of registrations.values()) {
+ if (handle.isActive) {
+ handle.unregister()
+ }
+ }
+ registrations.clear()
+ })
+}
+
+function resolveMaybeRefOrGetter(value: MaybeRefOrGetter): T {
+ return typeof value === 'function' ? (value as () => T)() : unref(value)
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5fc159a9..d5db5ac8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -431,6 +431,76 @@ importers:
specifier: 5.9.3
version: 5.9.3
+ examples/angular/injectHotkeySequences:
+ dependencies:
+ '@angular/common':
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2)
+ '@angular/compiler':
+ specifier: ^21.2.5
+ version: 21.2.5
+ '@angular/core':
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1)
+ '@angular/forms':
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2)
+ '@angular/platform-browser':
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))
+ '@angular/platform-browser-dynamic':
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.5)(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1)))
+ '@angular/router':
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2)
+ '@tanstack/angular-hotkeys':
+ specifier: ^0.6.0
+ version: link:../../../packages/angular-hotkeys
+ rxjs:
+ specifier: ~7.8.2
+ version: 7.8.2
+ tslib:
+ specifier: ^2.8.1
+ version: 2.8.1
+ zone.js:
+ specifier: ~0.16.1
+ version: 0.16.1
+ devDependencies:
+ '@angular-devkit/build-angular':
+ specifier: ^21.2.3
+ version: 21.2.3(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3))(@angular/compiler@21.2.5)(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.5.0)(chokidar@5.0.0)(jiti@2.6.1)(karma@6.4.4)(lightningcss@1.32.0)(typescript@5.9.3)(vitest@4.1.0(@types/node@25.5.0)(happy-dom@20.8.4)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)))(yaml@2.8.2)
+ '@angular/cli':
+ specifier: ^21.2.3
+ version: 21.2.3(@types/node@25.5.0)(chokidar@5.0.0)
+ '@angular/compiler-cli':
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3)
+ '@types/jasmine':
+ specifier: ~6.0.0
+ version: 6.0.0
+ jasmine-core:
+ specifier: ~6.1.0
+ version: 6.1.0
+ karma:
+ specifier: ~6.4.4
+ version: 6.4.4
+ karma-chrome-launcher:
+ specifier: ~3.2.0
+ version: 3.2.0
+ karma-coverage:
+ specifier: ~2.2.1
+ version: 2.2.1
+ karma-jasmine:
+ specifier: ~5.1.0
+ version: 5.1.0(karma@6.4.4)
+ karma-jasmine-html-reporter:
+ specifier: ~2.2.0
+ version: 2.2.0(jasmine-core@6.1.0)(karma-jasmine@5.1.0(karma@6.4.4))(karma@6.4.4)
+ typescript:
+ specifier: 5.9.3
+ version: 5.9.3
+
examples/angular/injectHotkeys:
dependencies:
'@angular/common':
@@ -696,6 +766,31 @@ importers:
specifier: ^8.0.1
version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
+ examples/preact/useHotkeySequences:
+ dependencies:
+ '@tanstack/preact-hotkeys':
+ specifier: ^0.6.0
+ version: link:../../../packages/preact-hotkeys
+ preact:
+ specifier: ^10.29.0
+ version: 10.29.0
+ devDependencies:
+ '@preact/preset-vite':
+ specifier: ^2.10.5
+ version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.57.1)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))
+ '@tanstack/preact-devtools':
+ specifier: 0.10.0
+ version: 0.10.0(csstype@3.2.3)(preact@10.29.0)(solid-js@1.9.11)
+ '@tanstack/preact-hotkeys-devtools':
+ specifier: ^0.5.0
+ version: link:../../../packages/preact-hotkeys-devtools
+ typescript:
+ specifier: 5.9.3
+ version: 5.9.3
+ vite:
+ specifier: ^8.0.1
+ version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
+
examples/preact/useHotkeys:
dependencies:
'@tanstack/preact-hotkeys':
@@ -916,6 +1011,40 @@ importers:
specifier: ^8.0.1
version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
+ examples/react/useHotkeySequences:
+ dependencies:
+ '@tanstack/react-hotkeys':
+ specifier: ^0.6.0
+ version: link:../../../packages/react-hotkeys
+ react:
+ specifier: ^19.2.4
+ version: 19.2.4
+ react-dom:
+ specifier: ^19.2.4
+ version: 19.2.4(react@19.2.4)
+ devDependencies:
+ '@tanstack/react-devtools':
+ specifier: 0.10.0
+ version: 0.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11)
+ '@tanstack/react-hotkeys-devtools':
+ specifier: ^0.5.0
+ version: link:../../../packages/react-hotkeys-devtools
+ '@types/react':
+ specifier: ^19.2.14
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.14)
+ '@vitejs/plugin-react':
+ specifier: ^6.0.1
+ version: 6.0.1(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))
+ typescript:
+ specifier: 5.9.3
+ version: 5.9.3
+ vite:
+ specifier: ^8.0.1
+ version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
+
examples/react/useHotkeys:
dependencies:
'@tanstack/react-hotkeys':
@@ -1100,6 +1229,28 @@ importers:
specifier: ^2.11.11
version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))
+ examples/solid/createHotkeySequences:
+ dependencies:
+ '@tanstack/solid-devtools':
+ specifier: 0.8.0
+ version: 0.8.0(csstype@3.2.3)(solid-js@1.9.11)
+ '@tanstack/solid-hotkeys':
+ specifier: ^0.6.0
+ version: link:../../../packages/solid-hotkeys
+ '@tanstack/solid-hotkeys-devtools':
+ specifier: ^0.5.0
+ version: link:../../../packages/solid-hotkeys-devtools
+ solid-js:
+ specifier: ^1.9.11
+ version: 1.9.11
+ devDependencies:
+ vite:
+ specifier: ^8.0.1
+ version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
+ vite-plugin-solid:
+ specifier: ^2.11.11
+ version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))
+
examples/solid/createHotkeys:
dependencies:
'@tanstack/solid-devtools':
@@ -1226,6 +1377,25 @@ importers:
specifier: ^8.0.1
version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
+ examples/svelte/create-hotkey-sequences:
+ dependencies:
+ '@tanstack/svelte-hotkeys':
+ specifier: 0.6.0
+ version: link:../../../packages/svelte-hotkeys
+ svelte:
+ specifier: ^5.54.1
+ version: 5.54.1
+ devDependencies:
+ '@sveltejs/vite-plugin-svelte':
+ specifier: ^7.0.0
+ version: 7.0.0(svelte@5.54.1)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))
+ typescript:
+ specifier: 5.9.3
+ version: 5.9.3
+ vite:
+ specifier: ^8.0.1
+ version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
+
examples/svelte/create-hotkeys:
dependencies:
'@tanstack/svelte-hotkeys':
@@ -1408,6 +1578,31 @@ importers:
specifier: ^8.0.1
version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
+ examples/vue/useHotkeySequences:
+ dependencies:
+ '@tanstack/vue-hotkeys':
+ specifier: ^0.6.0
+ version: link:../../../packages/vue-hotkeys
+ vue:
+ specifier: ^3.5.30
+ version: 3.5.30(typescript@5.9.3)
+ devDependencies:
+ '@tanstack/vue-devtools':
+ specifier: ^0.2.14
+ version: 0.2.14(csstype@3.2.3)(solid-js@1.9.11)
+ '@tanstack/vue-hotkeys-devtools':
+ specifier: ^0.5.0
+ version: link:../../../packages/vue-hotkeys-devtools
+ '@vitejs/plugin-vue':
+ specifier: ^6.0.5
+ version: 6.0.5(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
+ typescript:
+ specifier: 5.9.3
+ version: 5.9.3
+ vite:
+ specifier: ^8.0.1
+ version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
+
examples/vue/useHotkeys:
dependencies:
'@tanstack/vue-hotkeys':