+ `
+ }
+}
+```
+
+## Under the Hood
+
+The `HotkeyRecorderController` creates a `HotkeyRecorder` class instance and subscribes to its reactive state via the recorder's TanStack Store. The class manages its own keyboard event listeners and state, and the controller handles cleanup on disconnect.
diff --git a/docs/framework/lit/guides/hotkeys.md b/docs/framework/lit/guides/hotkeys.md
new file mode 100644
index 00000000..0e0ba535
--- /dev/null
+++ b/docs/framework/lit/guides/hotkeys.md
@@ -0,0 +1,317 @@
+---
+title: Hotkeys Guide
+id: hotkeys
+---
+
+The `@hotkey` decorator is the primary way to register keyboard shortcuts in Lit applications. It binds a hotkey to a class method, automatically registering when the element connects to the DOM and unregistering when it disconnects. For more dynamic use cases, the `HotkeyController` provides imperative control over hotkey registration.
+
+Both approaches wrap the singleton `HotkeyManager` with automatic lifecycle management tied to Lit's `connectedCallback` / `disconnectedCallback`.
+
+## Basic Usage
+
+### The `@hotkey` Decorator
+
+Decorate any method to have it called when a hotkey is pressed:
+
+```ts
+import { LitElement, html } from 'lit'
+import { customElement } from 'lit/decorators.js'
+import { hotkey } from '@tanstack/lit-hotkeys'
+
+@customElement('my-editor')
+class MyEditor extends LitElement {
+ @hotkey('Mod+S')
+ save() {
+ saveDocument()
+ }
+
+ render() {
+ return html`
Press Cmd+S (Mac) or Ctrl+S (Windows) to save
`
+ }
+}
+```
+
+The callback receives the original `KeyboardEvent` as the first argument and a `HotkeyCallbackContext` as the second:
+
+```ts
+@hotkey('Mod+S')
+save(event: KeyboardEvent, context: HotkeyCallbackContext) {
+ console.log(context.hotkey)
+ console.log(context.parsedHotkey)
+}
+```
+
+### The `HotkeyController`
+
+For cases where you need to construct the hotkey dynamically or pass a callback that isn't a class method, use `HotkeyController` directly:
+
+```ts
+import { LitElement, html } from 'lit'
+import { customElement } from 'lit/decorators.js'
+import { HotkeyController } from '@tanstack/lit-hotkeys'
+
+@customElement('my-editor')
+class MyEditor extends LitElement {
+ private saveHotkey = new HotkeyController(
+ this,
+ 'Mod+S',
+ () => this.save(),
+ )
+
+ constructor() {
+ super()
+ this.addController(this.saveHotkey)
+ }
+
+ private save() {
+ saveDocument()
+ }
+
+ render() {
+ return html`
Press Cmd+S (Mac) or Ctrl+S (Windows) to save
`
+ }
+}
+```
+
+## Default Options
+
+When you register a hotkey without passing options, or when you omit specific options, the following defaults apply:
+
+```ts
+@hotkey('Mod+S', {
+ enabled: true,
+ preventDefault: true,
+ stopPropagation: true,
+ eventType: 'keydown',
+ requireReset: false,
+ ignoreInputs: undefined, // smart default: false for Mod+S, true for single keys
+ platform: undefined, // auto-detected
+ conflictBehavior: 'warn',
+})
+save() { /* ... */ }
+```
+
+If you omit `target`, the Lit adapter resolves it when the controller connects: it listens on `document` in the browser, and skips registration in non-DOM environments.
+
+### Why These Defaults?
+
+Most hotkey registrations are intended to override default browser behavior — such as using `Mod+S` to save a document instead of showing the browser's "Save Page" dialog. To make this easy and consistent, the library sets `preventDefault` and `stopPropagation` to `true` by default, ensuring your hotkey handlers take precedence.
+
+#### Smart Input Handling: `ignoreInputs`
+
+The `ignoreInputs` option strikes a balance between accessibility and usability. By default, hotkeys involving `Ctrl`/`Meta` modifiers (like `Mod+S`) and the `Escape` key fire even when focus is inside input elements (text fields, text areas, etc.) and button-type inputs (`type="button"`, `"submit"`, or `"reset"`). Single key shortcuts or those using only `Shift`/`Alt` are ignored within non-button inputs to prevent interference with normal typing.
+
+#### Hotkey Conflicts: `conflictBehavior`
+
+When you register a hotkey that is already registered elsewhere in your app, the library logs a warning by default (`conflictBehavior: 'warn'`). This helps catch accidental duplicate bindings during development.
+
+## Hotkey Options
+
+### `enabled`
+
+Controls whether the hotkey is active. Defaults to `true`.
+
+```ts
+@hotkey('Mod+S', { enabled: true })
+save() { saveDocument() }
+```
+
+### `preventDefault`
+
+Automatically calls `event.preventDefault()` when the hotkey fires. Defaults to `true`.
+
+```ts
+// Browser default is prevented (default behavior)
+@hotkey('Mod+S')
+save() { saveDocument() }
+
+// Opt out when you want the browser's default behavior
+@hotkey('Mod+P', { preventDefault: false })
+print() { customPrint() }
+```
+
+### `stopPropagation`
+
+Calls `event.stopPropagation()` when the hotkey fires. Defaults to `true`.
+
+```ts
+// Event propagation is stopped (default behavior)
+@hotkey('Escape')
+close() { closeModal() }
+
+// Opt out when you need the event to bubble
+@hotkey('Escape', { stopPropagation: false })
+close() { closeModal() }
+```
+
+### `eventType`
+
+Whether to listen on `keydown` (default) or `keyup`.
+
+```ts
+// Fire when the key is released
+@hotkey('Shift', { eventType: 'keyup' })
+deactivateMode() { this.shiftMode = false }
+```
+
+### `requireReset`
+
+When `true`, the hotkey fires only once per key press. The key must be released and pressed again to fire again. Defaults to `false`.
+
+```ts
+// Only fires once per Escape press, not on key repeat
+@hotkey('Escape', { requireReset: true })
+closePanel() { this.panelOpen = false }
+```
+
+### `ignoreInputs`
+
+When `true`, the hotkey will not fire when the user is focused on a text input, textarea, select, or contentEditable element. Button-type inputs (`type="button"`, `"submit"`, `"reset"`) are not ignored. When unset, a smart default applies: `Ctrl`/`Meta` shortcuts and `Escape` fire in inputs; single keys and `Shift`/`Alt` combos are ignored.
+
+```ts
+// Single key — ignored in inputs by default (smart default)
+@hotkey('K')
+openSearch() { /* ... */ }
+
+// Mod+S and Escape — fire in inputs by default (smart default)
+@hotkey('Mod+S')
+save() { /* ... */ }
+
+// Override: force a single key to fire in inputs
+@hotkey('Enter', { ignoreInputs: false })
+submit() { /* ... */ }
+```
+
+### `target`
+
+The DOM element to attach the event listener to. When omitted, the Lit adapter resolves `document` at connect time in the browser. Can be a DOM element, `document`, or `window`. Pass `null` to intentionally skip registration.
+
+```ts
+import { LitElement, html } from 'lit'
+import { customElement } from 'lit/decorators.js'
+import { createRef, ref } from 'lit/directives/ref.js'
+import { HotkeyController } from '@tanstack/lit-hotkeys'
+
+@customElement('my-panel')
+class MyPanel extends LitElement {
+ private panelRef = createRef()
+ private escapeHotkey?: HotkeyController
+
+ firstUpdated() {
+ if (!this.panelRef.value) return
+
+ this.escapeHotkey = new HotkeyController(
+ this,
+ 'Escape',
+ () => this.dispatchEvent(new CustomEvent('close')),
+ { target: this.panelRef.value },
+ )
+ this.addController(this.escapeHotkey)
+ }
+
+ render() {
+ return html`
+
+
Press Escape while focused here to close
+
+ `
+ }
+}
+```
+
+> [!NOTE]
+> When using a scoped target, make sure the element is focusable (has `tabindex`) so it can receive keyboard events.
+
+### `conflictBehavior`
+
+Controls what happens when you register a hotkey that's already registered. Options:
+
+- `'warn'` (default) — Logs a warning but allows the registration
+- `'error'` — Throws an error
+- `'replace'` — Replaces the existing registration
+- `'allow'` — Allows multiple registrations silently
+
+```ts
+@hotkey('Mod+S', { conflictBehavior: 'replace' })
+save() { saveDocument() }
+```
+
+### `platform`
+
+Override the auto-detected platform. Useful for testing or for applications that need to force a specific platform behavior.
+
+```ts
+@hotkey('Mod+S', { platform: 'mac' })
+save() { saveDocument() }
+```
+
+## Automatic Cleanup
+
+Both the `@hotkey` decorator and `HotkeyController` automatically unregister the hotkey when the element is disconnected from the DOM:
+
+```ts
+@customElement('temporary-panel')
+class TemporaryPanel extends LitElement {
+ // Automatically registered on connect, unregistered on disconnect
+ @hotkey('Escape')
+ close() { this.remove() }
+
+ render() {
+ return html`
Panel content
`
+ }
+}
+```
+
+## Multiple Hotkeys
+
+Register as many hotkeys as you need. Each `@hotkey` decorator is independent:
+
+```ts
+@customElement('my-editor')
+class MyEditor extends LitElement {
+ @hotkey('Mod+S')
+ save() { saveDocument() }
+
+ @hotkey('Mod+Z')
+ undo() { undoAction() }
+
+ @hotkey('Mod+Shift+Z')
+ redo() { redoAction() }
+
+ @hotkey('Mod+F')
+ search() { openSearch() }
+
+ @hotkey('Escape')
+ dismiss() { closeDialog() }
+}
+```
+
+## Choosing Between Decorator and Controller
+
+| | `@hotkey` Decorator | `HotkeyController` |
+|---|---|---|
+| **Best for** | Static, declarative method binding | Dynamic hotkeys, programmatic control |
+| **Registration** | Automatic via `connectedCallback` | Automatic via `hostConnected` |
+| **Cleanup** | Automatic via `disconnectedCallback` | Automatic via `hostDisconnected` |
+| **Dynamic hotkeys** | No (hotkey is fixed at decoration time) | Yes (can construct hotkey at runtime) |
+| **Callback binding** | Bound to the host element automatically | Bound to the host element automatically |
+
+Use the `@hotkey` decorator for the common case of binding a static shortcut to a method. Use `HotkeyController` when you need to construct the hotkey string dynamically or manage registration imperatively.
+
+## The Hotkey Manager
+
+Under the hood, both the decorator and controller use the singleton `HotkeyManager`. You can access the manager directly when needed:
+
+```ts
+import { getHotkeyManager } from '@tanstack/lit-hotkeys'
+
+const manager = getHotkeyManager()
+
+// Check if a hotkey is registered
+manager.isRegistered('Mod+S')
+
+// Get total number of registrations
+manager.getRegistrationCount()
+```
+
+The manager attaches event listeners per target element, so only elements that have registered hotkeys receive listeners. This is more efficient than a single global listener.
diff --git a/docs/framework/lit/guides/key-state-tracking.md b/docs/framework/lit/guides/key-state-tracking.md
new file mode 100644
index 00000000..82fbc098
--- /dev/null
+++ b/docs/framework/lit/guides/key-state-tracking.md
@@ -0,0 +1,227 @@
+---
+title: Key State Tracking Guide
+id: key-state-tracking
+---
+
+TanStack Hotkeys provides three Lit **reactive controllers** for tracking the real-time state of keyboard keys. These are useful for building UIs that respond to modifier keys being held, displaying active key states, or implementing hold-to-activate features.
+
+
+## `HeldKeysController`
+
+Tracks all currently held key names. Exposes a reactive **`value`** getter: `Array`.
+
+```ts
+import { LitElement, html } from 'lit'
+import { customElement } from 'lit/decorators.js'
+import { HeldKeysController } from '@tanstack/lit-hotkeys'
+
+@customElement('key-display')
+class KeyDisplay extends LitElement {
+ private heldKeys = new HeldKeysController(this)
+
+ render() {
+ const keys = this.heldKeys.value
+ return html`
+
+ `
+ }
+}
+```
+
+Use this when you need to distinguish left vs. right modifiers (or other physical keys).
+
+## `KeyHoldController`
+
+Tracks whether **one** specific key is held. Exposes **`value`**: `boolean`. Updates the host only when **that** key’s held state changes (not on every unrelated key press).
+
+```ts
+import { LitElement, html } from 'lit'
+import { customElement } from 'lit/decorators.js'
+import { KeyHoldController } from '@tanstack/lit-hotkeys'
+
+@customElement('modifier-indicators')
+class ModifierIndicators extends LitElement {
+ private shift = new KeyHoldController(this, 'Shift')
+ private ctrl = new KeyHoldController(this, 'Control')
+ private alt = new KeyHoldController(this, 'Alt')
+ private meta = new KeyHoldController(this, 'Meta')
+
+ render() {
+ return html`
+
+ Shift
+ Ctrl
+ Alt
+ Meta
+
+ `
+ }
+}
+```
+
+## Common patterns
+
+### Hold-to-reveal UI
+
+Show extra actions while Shift is held:
+
+```ts
+import { LitElement, html } from 'lit'
+import { customElement, property } from 'lit/decorators.js'
+import { KeyHoldController } from '@tanstack/lit-hotkeys'
+
+@customElement('file-item')
+class FileItem extends LitElement {
+ @property({ type: String }) fileName = ''
+
+ private shift = new KeyHoldController(this, 'Shift')
+
+ render() {
+ return html`
+
+ `
+ }
+}
+```
+
+## Platform quirks
+
+The underlying `KeyStateTracker` handles several platform-specific issues:
+
+### macOS modifier key behavior
+
+On macOS, when a modifier key is held and a non-modifier key is pressed, the OS sometimes swallows the `keyup` event for the non-modifier key. TanStack Hotkeys detects and handles this automatically so held key state stays accurate.
+
+### Window blur
+
+When the browser window loses focus, all held keys are automatically cleared. This prevents “stuck” keys after the user tabs away and releases keys outside the window.
+
+## Under the hood
+
+The three controllers subscribe to the singleton `KeyStateTracker` store from `@tanstack/hotkeys`. The tracker manages its own event listeners on `document` and maintains state in a TanStack Store, which the controllers read reactively.
+
+```ts
+import { getKeyStateTracker } from '@tanstack/lit-hotkeys'
+
+const tracker = getKeyStateTracker()
+
+tracker.getHeldKeys() // string[]
+tracker.store.state.heldCodes // Record
+tracker.isKeyHeld('Shift') // boolean
+tracker.isAnyKeyHeld(['Shift', 'Control']) // boolean
+tracker.areAllKeysHeld(['Shift', 'Control']) // boolean
+```
diff --git a/docs/framework/lit/guides/sequence-recording.md b/docs/framework/lit/guides/sequence-recording.md
new file mode 100644
index 00000000..eec15a0a
--- /dev/null
+++ b/docs/framework/lit/guides/sequence-recording.md
@@ -0,0 +1,122 @@
+---
+title: Sequence Recording Guide
+id: sequence-recording
+---
+
+TanStack Hotkeys provides the `HotkeySequenceRecorderController` for building UIs where users record **multi-chord sequences** (Vim-style shortcuts). Each step is captured like a single hotkey chord; users finish with **Enter** by default, or you can use manual commit and optional idle timeout.
+
+## Basic Usage
+
+```ts
+import { LitElement, html, nothing } from 'lit'
+import { customElement } from 'lit/decorators.js'
+import { HotkeySequenceRecorderController, formatForDisplay } from '@tanstack/lit-hotkeys'
+import type { HotkeySequence } from '@tanstack/lit-hotkeys'
+
+@customElement('sequence-recorder')
+class SequenceRecorder extends LitElement {
+ private recorder = new HotkeySequenceRecorderController(this, {
+ onRecord: (sequence: HotkeySequence) => {
+ console.log('Recorded:', sequence) // e.g., ["G", "G"]
+ },
+ })
+
+ render() {
+ const { isRecording, steps, recordedSequence } = this.recorder
+ return html`
+
+
+ ${isRecording
+ ? html``
+ : nothing}
+
+ `
+ }
+}
+```
+
+## Controller API
+
+`HotkeySequenceRecorderController` exposes the following reactive getters and methods:
+
+| Member | Type | Description |
+|--------|------|-------------|
+| `isRecording` | `boolean` | Whether the recorder is currently listening |
+| `steps` | `HotkeySequence` | Chords captured in the current session |
+| `recordedSequence` | `HotkeySequence \| null` | Last committed sequence |
+| `startRecording()` | `() => void` | Start a new recording session |
+| `stopRecording()` | `() => void` | Stop without calling `onRecord` |
+| `cancelRecording()` | `() => void` | Stop and call `onCancel` |
+| `commitRecording()` | `() => void` | Commit current `steps` (no-op if empty) |
+| `setOptions(opts)` | `(Partial) => void` | Update options at runtime |
+
+The controller registers itself with the host in its constructor, subscribes to the underlying `HotkeySequenceRecorder` store on `hostConnected`, and cleans up on `hostDisconnected`.
+
+## Options
+
+Pass options as the second argument to the constructor:
+
+```ts
+new HotkeySequenceRecorderController(this, {
+ onRecord: (sequence) => { /* called when a sequence is committed */ },
+ onCancel: () => { /* called when recording is cancelled */ },
+ onClear: () => { /* called when cleared via Backspace with no steps */ },
+ commitKeys: 'enter', // or 'none'
+ idleTimeoutMs: 2000,
+})
+```
+
+### `onRecord`
+
+Called when a sequence is committed (including `[]` when cleared via Backspace with no steps). Receives the recorded `HotkeySequence` array.
+
+### `onCancel`
+
+Called when recording is cancelled (either by pressing Escape or calling `cancelRecording()`).
+
+### `onClear`
+
+Called when the sequence is cleared (by pressing Backspace or Delete during recording when no steps remain).
+
+### `commitKeys`
+
+Controls how the user finishes recording from the keyboard:
+
+- `'enter'` (default) — plain Enter (no modifiers) commits when at least one step exists.
+- `'none'` — only `commitRecording()` or `idleTimeoutMs` finishes recording; plain Enter can be recorded as a chord.
+
+### `commitOnEnter`
+
+When `commitKeys` is `'enter'`, set to `false` to treat Enter as a normal chord. Use `commitRecording()` or idle timeout to finish instead.
+
+### `idleTimeoutMs`
+
+Milliseconds of inactivity **after the last completed chord** to auto-commit. The timer does not run while waiting for the **first** chord.
+
+## Recording Behavior
+
+| Input | Behavior |
+|-------|----------|
+| Valid chord | Appended to `steps`; listener stays active |
+| Enter (no modifiers), `commitKeys: 'enter'`, `steps.length >= 1` | Commits and calls `onRecord` |
+| Escape | Cancels; calls `onCancel` |
+| Backspace / Delete (no modifiers) | Removes last step, or if empty runs `onClear` + `onRecord([])` and stops |
+
+Recorded chords use portable `Mod` format, same as `HotkeyRecorderController`.
+
+## Under the Hood
+
+The `HotkeySequenceRecorderController` creates a `HotkeySequenceRecorder` class instance and subscribes to its reactive state via `@tanstack/store`. The class manages its own keyboard event listeners and state, and the controller handles cleanup on disconnect.
diff --git a/docs/framework/lit/guides/sequences.md b/docs/framework/lit/guides/sequences.md
new file mode 100644
index 00000000..f3a44943
--- /dev/null
+++ b/docs/framework/lit/guides/sequences.md
@@ -0,0 +1,303 @@
+---
+title: Sequences Guide
+id: sequences
+---
+
+TanStack Hotkeys supports multi-key sequences -- shortcuts where you press keys one after another rather than simultaneously. This is commonly used for Vim-style navigation, cheat codes, or multi-step commands.
+
+In Lit, registration is **declarative** via the `@hotkeySequence` decorator, or **imperative** via `HotkeySequenceController` when the sequence or options are built at runtime. Both use the same singleton `SequenceManager`.
+
+## Basic Usage
+
+### The `@hotkeySequence` decorator
+
+Decorate a method to run when the user completes the key sequence:
+
+```ts
+import { LitElement, html } from 'lit'
+import { customElement } from 'lit/decorators.js'
+import { hotkeySequence } from '@tanstack/lit-hotkeys'
+
+@customElement('vim-view')
+class VimView extends LitElement {
+ @hotkeySequence(['G', 'G'])
+ scrollTop() {
+ window.scrollTo({ top: 0, behavior: 'smooth' })
+ }
+
+ render() {
+ return html`
Press g then g to scroll to top
`
+ }
+}
+```
+
+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.
+
+The method receives the `KeyboardEvent` and [`HotkeyCallbackContext`](../../../reference/interfaces/HotkeyCallbackContext.md) like `@hotkey`:
+
+```ts
+import type { HotkeyCallbackContext } from '@tanstack/lit-hotkeys'
+
+@hotkeySequence(['G', 'G'])
+scrollTop(event: KeyboardEvent, context: HotkeyCallbackContext) {
+ console.log(context.hotkey)
+}
+```
+
+### `HotkeySequenceController`
+
+Use the controller when the sequence or callback cannot be expressed as a static decorator (e.g. data-driven shortcuts):
+
+```ts
+import { LitElement, html } from 'lit'
+import { customElement } from 'lit/decorators.js'
+import { HotkeySequenceController } from '@tanstack/lit-hotkeys'
+
+@customElement('vim-view')
+class VimView extends LitElement {
+ private scrollTopSeq = new HotkeySequenceController(
+ this,
+ ['G', 'G'],
+ () => this.scrollTop(),
+ )
+
+ constructor() {
+ super()
+ this.addController(this.scrollTopSeq)
+ }
+
+ private scrollTop() {
+ window.scrollTo({ top: 0, behavior: 'smooth' })
+ }
+
+ render() {
+ return html`
Press g then g to scroll to top
`
+ }
+}
+```
+
+## Many sequences at once
+
+Register several sequences on the same element by applying **multiple** `@hotkeySequence` decorators (one per method), or by adding **multiple** `HotkeySequenceController` instances with `addController`. There is no Lit equivalent to React’s `useHotkeySequences` hook; several decorators on one class are the idiomatic pattern.
+
+```ts
+@customElement('vim-navigation')
+class VimNavigation extends LitElement {
+ @hotkeySequence(['G', 'G'])
+ goTop() {
+ scrollToTop()
+ }
+
+ @hotkeySequence(['G', 'Shift+G'])
+ goBottom() {
+ scrollToBottom()
+ }
+
+ @hotkeySequence(['D', 'D'], { timeout: 500 })
+ deleteLine() {
+ deleteCurrentLine()
+ }
+
+ render() {
+ return html``
+ }
+}
+```
+
+## Sequence Options
+
+Pass options as the **second** argument to `@hotkeySequence` (or to `HotkeySequenceController`):
+
+```ts
+@hotkeySequence(['G', 'G'], {
+ timeout: 1000, // Time allowed between keys (ms)
+ enabled: true, // Whether the sequence is active at connect time
+})
+scrollTop() {
+ window.scrollTo({ top: 0, behavior: 'smooth' })
+}
+```
+
+### `timeout`
+
+The maximum time (in milliseconds) allowed between consecutive key presses. If the user takes longer than this between any two keys, the sequence resets. Defaults to `1000` (1 second).
+
+```ts
+@hotkeySequence(['D', 'D'], { timeout: 500 })
+deleteLine() {
+ deleteCurrentLine()
+}
+
+@hotkeySequence(['Shift+Z', 'Shift+Z'], { timeout: 2000 })
+forceQuit() {
+ quitWithoutSaving()
+}
+```
+
+### `enabled`
+
+Controls whether the sequence is registered when the element connects. Defaults to `true`. If `enabled` is `false` at connection time, registration is skipped. To turn sequences on or off later, reconnect the element, or create a new `HotkeySequenceController` when your state changes.
+
+```ts
+@hotkeySequence(['G', 'G'], { enabled: true })
+scrollToTop() {
+ window.scrollTo({ top: 0, behavior: 'smooth' })
+}
+```
+
+### Default options
+
+When you omit options, the library uses the same defaults as the core [`SequenceOptions`](../../../reference/interfaces/SequenceOptions.md): `timeout: 1000`, `preventDefault` / `stopPropagation` enabled, smart `ignoreInputs`, and platform auto-detection. If you omit `target`, the Lit adapter resolves it to `document` when the controller connects in the browser.
+
+## Sequences with Modifiers
+
+Each step in a sequence can include modifiers:
+
+```ts
+@hotkeySequence(['Mod+K', 'Mod+C'])
+commentSelection() {
+ commentSelection()
+}
+
+@hotkeySequence(['G', 'Shift+G'])
+scrollBottom() {
+ window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })
+}
+```
+
+## Chained modifier chords
+
+You can repeat the same modifier across consecutive steps — for example `Shift+R` then `Shift+T`:
+
+```ts
+@hotkeySequence(['Shift+R', 'Shift+T'])
+chordSequence() {
+ runAfterChords()
+}
+```
+
+### Modifier-only keys between steps
+
+While a sequence is in progress, **modifier-only** keydown events (Shift, Control, Alt, or Meta pressed alone, with no letter or other key) are ignored. They do not advance the sequence and they do **not** reset progress. That way a user can tap Shift (or hold it) between chords such as `Shift+R` and `Shift+T` without breaking the sequence — similar to Vim-style flows where a modifier may be pressed before the next chord.
+
+## Common Sequence Patterns
+
+### Vim-style navigation
+
+```ts
+@customElement('vim-navigation')
+class VimNavigation extends LitElement {
+ @hotkeySequence(['G', 'G'])
+ goTop() {
+ scrollToTop()
+ }
+
+ @hotkeySequence(['G', 'Shift+G'])
+ goBottom() {
+ scrollToBottom()
+ }
+
+ @hotkeySequence(['D', 'D'])
+ deleteLine() {
+ deleteCurrentLine()
+ }
+
+ @hotkeySequence(['D', 'W'])
+ deleteWord() {
+ deleteCurrentWord()
+ }
+
+ @hotkeySequence(['C', 'I', 'W'])
+ changeInnerWord() {
+ changeInnerWordImpl()
+ }
+
+ render() {
+ return html``
+ }
+}
+```
+
+### Konami code
+
+```ts
+@hotkeySequence(
+ [
+ 'ArrowUp',
+ 'ArrowUp',
+ 'ArrowDown',
+ 'ArrowDown',
+ 'ArrowLeft',
+ 'ArrowRight',
+ 'ArrowLeft',
+ 'ArrowRight',
+ 'B',
+ 'A',
+ ],
+ { timeout: 2000 },
+)
+enableEasterEgg() {
+ showEasterEgg()
+}
+```
+
+### Multi-step commands
+
+```ts
+@hotkeySequence(['H', 'E', 'L', 'P'])
+openHelp() {
+ showHelp()
+}
+```
+
+## How sequences work
+
+The `SequenceManager` (singleton) handles all sequence registrations. When a key is pressed:
+
+1. It checks if the key matches the next expected step in any registered sequence
+2. If it matches, the sequence advances to the next step
+3. If the timeout expires between steps, the sequence resets
+4. When all steps are completed, the callback fires
+5. Modifier-only keydowns are ignored (they neither advance nor reset the sequence)
+
+### Overlapping sequences
+
+Multiple sequences can share the same prefix. The manager tracks progress for each sequence independently:
+
+```ts
+@hotkeySequence(['D', 'D'])
+dd() {
+ deleteLine()
+}
+
+@hotkeySequence(['D', 'W'])
+dw() {
+ deleteWord()
+}
+
+@hotkeySequence(['D', 'I', 'W'])
+diw() {
+ deleteInnerWord()
+}
+```
+
+After pressing `D`, the manager waits for the next key to determine which sequence to complete.
+
+## The sequence manager
+
+Under the hood, `@hotkeySequence` and `HotkeySequenceController` use the singleton `SequenceManager`. You can also use the core `createSequenceMatcher` function for standalone sequence matching without the singleton:
+
+```ts
+import { createSequenceMatcher } from '@tanstack/lit-hotkeys'
+
+const matcher = createSequenceMatcher(['G', 'G'], {
+ timeout: 1000,
+})
+
+document.addEventListener('keydown', (e) => {
+ if (matcher.match(e)) {
+ console.log('Sequence completed!')
+ }
+ console.log('Progress:', matcher.getProgress()) // e.g., 1/2
+})
+```
diff --git a/docs/framework/lit/quick-start.md b/docs/framework/lit/quick-start.md
new file mode 100644
index 00000000..4b2c7316
--- /dev/null
+++ b/docs/framework/lit/quick-start.md
@@ -0,0 +1,291 @@
+---
+title: Quick Start
+id: quick-start
+---
+
+## Installation
+
+Don't have TanStack Hotkeys installed yet? See the [Installation](../../installation) page for instructions.
+
+## Your First Hotkey
+
+The Lit adapter offers two ways to register hotkeys: **decorators** for declarative method-level binding, and **controllers** for imperative, reactive state management.
+
+### Using the `@hotkey` Decorator
+
+The `@hotkey` decorator is the simplest way to bind a keyboard shortcut to a class method:
+
+```ts
+import { LitElement, html } from 'lit'
+import { customElement } from 'lit/decorators.js'
+import { hotkey } from '@tanstack/lit-hotkeys'
+
+@customElement('my-editor')
+class MyEditor extends LitElement {
+ @hotkey('Mod+S')
+ save() {
+ saveDocument()
+ }
+
+ render() {
+ return html`
Press Cmd+S (Mac) or Ctrl+S (Windows) to save
`
+ }
+}
+```
+
+### Using `HotkeyController`
+
+For more control, use the `HotkeyController` directly:
+
+```ts
+import { LitElement, html } from 'lit'
+import { customElement } from 'lit/decorators.js'
+import { HotkeyController } from '@tanstack/lit-hotkeys'
+
+@customElement('my-editor')
+class MyEditor extends LitElement {
+ private saveHotkey = new HotkeyController(
+ this,
+ 'Mod+S',
+ () => this.save(),
+ )
+
+ constructor() {
+ super()
+ this.addController(this.saveHotkey)
+ }
+
+ private save() {
+ saveDocument()
+ }
+
+ render() {
+ return html`
Press Cmd+S (Mac) or Ctrl+S (Windows) to save
`
+ }
+}
+```
+
+The `Mod` modifier automatically resolves to `Meta` (Command) on macOS and `Control` on Windows/Linux, so your shortcuts work across platforms without extra logic.
+
+## Common Patterns
+
+### Multiple Hotkeys
+
+Register as many hotkeys as you need with the `@hotkey` decorator:
+
+```ts
+@customElement('my-editor')
+class MyEditor extends LitElement {
+ @hotkey('Mod+S')
+ save() { saveDocument() }
+
+ @hotkey('Mod+Z')
+ undo() { undoAction() }
+
+ @hotkey('Mod+Shift+Z')
+ redo() { redoAction() }
+
+ @hotkey('Mod+F')
+ search() { openSearch() }
+
+ @hotkey('Escape')
+ dismiss() { closeDialog() }
+}
+```
+
+### Scoped Hotkeys
+
+Attach hotkeys to specific elements instead of the entire document using the `target` option. When the target comes from a ref, create the registration after the element has rendered:
+
+```ts
+import { LitElement, html } from 'lit'
+import { customElement } from 'lit/decorators.js'
+import { createRef, ref } from 'lit/directives/ref.js'
+import { HotkeyController } from '@tanstack/lit-hotkeys'
+
+@customElement('my-panel')
+class MyPanel extends LitElement {
+ private panelRef = createRef()
+ private escapeHotkey?: HotkeyController
+
+ firstUpdated() {
+ if (!this.panelRef.value) return
+
+ this.escapeHotkey = new HotkeyController(
+ this,
+ 'Escape',
+ () => this.closePanel(),
+ { target: this.panelRef.value },
+ )
+ this.addController(this.escapeHotkey)
+ }
+
+ private closePanel() {
+ this.dispatchEvent(new CustomEvent('close'))
+ }
+
+ render() {
+ return html`
+
+
Press Escape while focused here to close
+
+ `
+ }
+}
+```
+
+### Conditional Hotkeys
+
+Enable or disable hotkeys based on application state via the `enabled` option:
+
+```ts
+import { LitElement, html } from 'lit'
+import { customElement } from 'lit/decorators.js'
+import { hotkey } from '@tanstack/lit-hotkeys'
+
+@customElement('my-modal')
+class MyModal extends LitElement {
+ @hotkey('Escape', { enabled: true })
+ close() {
+ this.dispatchEvent(new CustomEvent('close'))
+ }
+}
+```
+
+### Multi-Key Sequences
+
+Register Vim-style key sequences with the `@hotkeySequence` decorator or `HotkeySequenceController`:
+
+```ts
+import { LitElement } from 'lit'
+import { customElement } from 'lit/decorators.js'
+import { hotkeySequence } from '@tanstack/lit-hotkeys'
+
+@customElement('vim-editor')
+class VimEditor extends LitElement {
+ @hotkeySequence(['G', 'G'])
+ scrollToTop() {
+ window.scrollTo({ top: 0 })
+ }
+
+ @hotkeySequence(['G', 'Shift+G'])
+ scrollToBottom() {
+ window.scrollTo({ top: document.body.scrollHeight })
+ }
+}
+```
+
+### Tracking Held Keys
+
+Display modifier key state for power-user UIs using `KeyHoldController` and `HeldKeysController`:
+
+```ts
+import { LitElement, html } from 'lit'
+import { customElement } from 'lit/decorators.js'
+import { KeyHoldController, HeldKeysController } from '@tanstack/lit-hotkeys'
+
+@customElement('status-bar')
+class StatusBar extends LitElement {
+ private shiftHold = new KeyHoldController(this, 'Shift')
+ private heldKeys = new HeldKeysController(this)
+
+ render() {
+ return html`
+