# Hotkey Recording Guide

Use `createHotkeyRecorder` to build a shortcut customization UI. Recording defaults to physical codes, producing values such as `Mod+[KeyS]`. Store that value directly and pass it to `createHotkey`. Use `formatForDisplay` for the label.

TanStack Hotkeys automatically suppresses registered hotkey and sequence callbacks while any recorder is active. You do not need to set `enabled` from `isRecording`. Registrations remain available for conflict detection, and recorded keys stay suppressed through repeats and key release.

## Reactive options

Recorder options support [property getters and functions returning options](./hotkeys.md#property-getters). Updated callbacks, validation, and recording settings apply during an active session without restarting it. Create getters in `init()` so they read the reactive component instance.

## Basic usage


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

class ShortcutSettings {
	binding: Hotkey = 'Mod+S'
	scope = createHotkeysScope()
	recorder!: AlpineHotkeyRecorder

	init() {
		this.recorder = this.scope.createHotkeyRecorder({
			onRecord: (hotkey) => { this.binding = hotkey },
			onClear: () => { this.binding = 'Mod+S' },
		})
		this.scope.createHotkey(() => this.binding, () => console.log('Saved'))
	}
	get label() { return formatForDisplay(this.binding) }
	destroy() { this.scope.destroy() }
}

Alpine.data('shortcutSettings', () => new ShortcutSettings())
```

```html
<div x-data="shortcutSettings">
	<kbd x-text="label"></kbd>
	<button type="button" @click="recorder.startRecording()">Record</button>
	<template x-if="recorder.isRecording">
		<div>
			<p>Press a shortcut. Escape cancels; Backspace resets the binding.</p>
			<button type="button" @click="recorder.cancelRecording()">Cancel</button>
		</div>
	</template>
</div>
```

This example stores the replacement binding in application state and restores `Mod+S` when the user clears it. Cancellation leaves the saved binding unchanged.

## Return value

| Property | Type | Meaning |
| --- | --- | --- |
| `isRecording` | `boolean` | Whether a session is active. |
| `recordedHotkey` | `Hotkey \| null` | The recorded binding, or `null` after starting, stopping, or cancelling. |
| `startRecording` | `() => void` | Start a new session. |
| `stopRecording` | `() => void` | Stop and clear recorder state without calling `onRecord` or `onCancel`. |
| `cancelRecording` | `() => void` | Stop, clear recorder state, and call `onCancel`. |

The state fields are reactive getters. Read `recorder.isRecording` and `recorder.recordedHotkey` where the framework tracks dependencies; destructuring them once captures a snapshot.

## Options

### `recordBy`

The default is `'code'`. On macOS, Option+S producing `ß` records `Alt+[KeyS]`, and Option+2 producing `™` records `Alt+[Digit2]`. Stored brackets preserve physical identity through serialization and registration. Existing authored strings such as `Mod+S` remain logical.

Set `recordBy: 'key'` to record the produced character. Code mode rejects an event without a usable code and never falls back to key mode. IME composition is ignored. AltGraph character entry is rejected in code mode; key mode preserves the character without synthetic Control/Alt while retaining Shift.

### `onRecord`

Receives the recorded `Hotkey` when the user enters a valid chord. A chord can be a single non-modifier key or a key with modifiers. Update your saved binding here.

### `onCancel`

Runs when Escape cancels a session or when you call `cancelRecording()`. Use it to exit an editing state without changing the saved binding.

### `onClear`

Runs when the user presses unmodified Backspace or Delete during recording. Clearing calls only `onClear`; it does not call `onRecord`. Your application decides whether to remove the binding or restore an initial value.

### Shared defaults and changing options

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. See [shared defaults](../quick-start.md#shared-defaults) for a complete example.

Pass an options getter to follow changing Alpine state: `scope.createHotkeyRecorder(() => ({ recordBy: this.recordBy, onRecord: this.saveBinding }))`. Read the latest component properties inside callbacks.

## Recording behavior

| Input | Behavior |
| --- | --- |
| Modifier alone | Wait for a non-modifier key. |
| Modifier plus a non-modifier key | Record the chord and finish. |
| Single non-modifier key, such as F1 | Record the key and finish. |
| Escape | Cancel. |
| Unmodified Backspace or Delete | Clear and call `onClear`. |
| Automatic key repeat or IME composition | Do not record a new chord. |

Recording events, repeats, and their key releases do not trigger registered hotkeys or sequences.

### `ignoreInputs`

This defaults to `true`. Normal typing in inputs, textareas, selects, and contentEditable elements passes through. Escape still cancels while an input is focused. Set `ignoreInputs: false` to capture shortcuts from a focused input.

### Mod auto-conversion

On macOS, Command+S becomes `Mod+[KeyS]`. Reusing that binding on Windows resolves `Mod` to Control while preserving the physical key position. Pass a `platform` option when detection must be overridden.

## Validation and conflicts

Supply these options alongside `onRecord`:

```ts
import type { HotkeyRecorderOptions } from '@tanstack/alpine-hotkeys'

const options: HotkeyRecorderOptions = {
	onRecord: (hotkey) => console.log('Accepted', hotkey),
	detectConflicts: {
		// Replace this with the ID from the live registration being edited.
		excludeIds: ['registration-being-edited'],
		target: document,
		eventType: 'keydown',
	},
	validate: (_hotkey, { parsedHotkey }) =>
		parsedHotkey.modifiers.length > 0 || 'Include a modifier.',
	onReject: ({ reason, message, conflicts }) => {
		console.log(reason, message, conflicts)
	},
}
```

`validate` returns `true` to accept, or `false` or a message to reject. Validation runs before commit. A rejected candidate leaves recording active. `onReject` reports `missing-code`, `alt-graph`, `invalid`, `validation`, or `conflict`, plus the candidate when available and conflicting views for a conflict.

`detectConflicts: true` checks enabled live registrations with the intended event type, defaulting to keydown, and overlapping targets, defaulting to document. Document and nested element scopes can conflict; disjoint widgets can reuse a binding. The options object supports `scope: 'all'`, `includeDisabled`, and an `exclude(registration)` predicate.

Checks include single bindings and sequence prefixes. Source events detect physical/logical overlap on the recorded layout. This is conservative collision detection: propagation, input filtering, match priority, external listeners, unmounted routes, and other layouts can change dispatch.

For checks outside recording, call `findHotkeyConflicts(bindingOrSequence, options)`. Without source `events`, it compares identities and prefixes rather than guessing which logical character a physical position produces.

## Building a shortcut settings UI

For several actions, keep the bindings in an array or object and record the ID of the action being edited. On `onRecord`, replace that action's binding and clear the editing ID. On `onCancel`, clear only the editing ID. Use the plural registration API for the current list.

```ts
// Inside init(), after creating this.recorder:
this.scope.createHotkeys(() => this.shortcuts.map((shortcut) => ({
	hotkey: shortcut.hotkey,
	callback: () => this.runAction(shortcut.id),
	options: { meta: { name: shortcut.name } },
})))
```

The [createHotkeyRecorder example](../examples/createHotkeyRecorder) includes multiple actions, editable names and descriptions, create/delete controls, reset and clear behavior, cancellation, and a live registry. The [kitchen sink](../examples/kitchen-sink) also demonstrates conflict feedback and physical versus logical recording.

## Under the hood

Alpine subscribes to the core TanStack Store and destroys the recorder with its scope. The core `HotkeyRecorder` owns the recording listeners. Store the accepted binding in application state rather than relying on recorder session state as your preferences store.
