TanStack
Getting Started

Quick Start

Install @tanstack/alpine-hotkeys with the installation instructions. This guide builds a save shortcut and shows the component lifecycle used by the other APIs.

Your first hotkey

ts
import Alpine from 'alpinejs'
import { createHotkeysScope, formatForDisplay } from '@tanstack/alpine-hotkeys'

class Editor {
	count = 0
	enabled = true
	scope = createHotkeysScope()
	label = formatForDisplay('Mod+S')

	init() {
		this.scope.createHotkey('Mod+S', () => this.count++,
			() => ({ enabled: this.enabled }))
	}

	destroy() {
		this.scope.destroy()
	}
}

Alpine.data('editor', () => new Editor())
Alpine.start()
html
<div x-data="editor">
	<button type="button" @click="enabled = !enabled">Toggle shortcut</button>
	<p>Press <kbd x-text="label"></kbd>. Saved <span x-text="count"></span> times.</p>
</div>

The Alpine adapter runs in the browser. In an application with server rendering, load it only on the client. Load the module after the markup is available. Call Alpine.start() once in your application entry point.

Press Command+S on macOS or Control+S on Windows and Linux. The count increases and the browser save dialog is prevented. The toggle disables execution while preserving the registration.

Mod resolves to the primary platform modifier. Use Mod+[KeyS] instead when the shortcut must follow the physical S position.

Registration and cleanup

Create one scope for each Alpine component. Register shortcuts in init() so getters and callbacks read the reactive component instance. Call scope.destroy() from the component's destroy() method. Destruction releases registrations, effects, Store subscriptions, and active recorders. Do not reuse a destroyed scope.

Pass getters for bindings and options that can change. A value such as { enabled: this.enabled } captures the value at registration time; () => ({ enabled: this.enabled }) follows Alpine state. State readers return an object with a reactive .value getter. Read it in a template or an Alpine effect instead of destructuring it once.

Common patterns

Multiple hotkeys

ts
this.scope.createHotkey('Mod+S', () => console.log('Save'))
this.scope.createHotkey('Mod+Z', () => console.log('Undo'))
this.scope.createHotkey('Mod+Shift+Z', () => console.log('Redo'))

Each registration is independent. For a dynamic list, use createHotkeys as shown in the hotkeys guide.

Scoped hotkeys

Pass an actual element as target. A null target defers registration until the element exists. Make the element focusable with tabindex="0". See the complete scoped example.

Conditional hotkeys

ts
this.scope.createHotkey('Mod+S', () => console.log("Save"), () => ({ enabled: this.enabled }))

Use Alpine reactive state for the enabled flag.

Multi-key sequences

ts
this.scope.createHotkeySequence(['G', 'G'], () => window.scrollTo({ top: 0 }))

Release G between presses. Automatic key repeats do not advance sequences.

Tracking held keys and displaying hints

ts
import Alpine from 'alpinejs'
import { createHotkeysScope } from '@tanstack/alpine-hotkeys'

Alpine.data('keyStatus', () => {
	const scope = createHotkeysScope()
	return {
		held: scope.createHeldKeys(),
		codes: scope.createHeldKeyCodes(),
		shift: scope.createKeyHold('Shift'),
		hint: scope.createHotkeyHint('Mod+S'),
		destroy() { scope.destroy() },
	}
})
html
<div x-data="keyStatus">
	<p x-text="held.value.join(' + ') || 'No keys held'"></p>
	<template x-for="key in held.value" :key="key">
		<p x-text="key + ': ' + codes.value[key]"></p>
	</template>
	<button type="button" x-show="shift.value">Delete permanently</button>
	<kbd x-show="hint.value">Save</kbd>
</div>

Use formatForDisplay(binding) for platform-specific labels. Keep the original binding in state; a display label is not a registration string.

Shared defaults

Pass defaults to createHotkeysScope. The scope accepts hotkey, hotkeySequence, hotkeyRecorder, and hotkeySequenceRecorder options. Pass a getter to follow Alpine state. Each component owns and destroys its scope. Call-specific options override scope defaults, and per-definition options override common options. Omitted options use the core defaults.

ts
import Alpine from 'alpinejs'
import { createHotkeysScope } from '@tanstack/alpine-hotkeys'

Alpine.data('scopedEditor', () => {
  const scope = createHotkeysScope({
    hotkey: { requireReset: true },
    hotkeySequence: { timeout: 1500 },
    hotkeyRecorder: { ignoreInputs: false },
    hotkeySequenceRecorder: { idleTimeoutMs: 2000 },
  })
  return {
    init() {
      scope.createHotkey('Mod+S', () => console.log('Saved'))
      scope.createHotkey('ArrowRight', () => console.log('Next'), { requireReset: false })
    },
    destroy() { scope.destroy() },
  }
})

For changing defaults, create the scope in init() with createHotkeysScope(() => ({ hotkey: { enabled: this.enabled } })). Creating the getter in init() makes this refer to Alpine's reactive component instance. Share a defaults object or getter between components when they need the same policy; give each component its own scope for cleanup. The $hotkeys magic uses core defaults, so use an explicit scope when supplying shared defaults.

Optional Alpine magic

Install hotkeysPlugin before Alpine.start() to get an element-owned $hotkeys scope:

ts
import Alpine from 'alpinejs'
import { hotkeysPlugin } from '@tanstack/alpine-hotkeys'

Alpine.plugin(hotkeysPlugin)
Alpine.start()
html
<div x-data="{ count: 0 }" x-init="$hotkeys.createHotkey('Mod+S', () => count++)">
	<span x-text="count"></span>
</div>

The plugin destroys that scope when Alpine destroys the owning element. Choose an explicit scope or the magic for a registration; using both registers it twice.

Examples and next steps