For AI agents: the complete documentation index is available at https://a3s-lab.github.io/UI/v0.4.1/en/llms.txt, the full documentation bundle is available at https://a3s-lab.github.io/UI/v0.4.1/en/llms-full.txt, and this page is available as Markdown at https://a3s-lab.github.io/UI/v0.4.1/en/components/hotkey-input.md.
Hotkey Input records one keyboard chord for a command assignment. It keeps the committed shortcut visible, makes recording explicit, and preserves the prior value when replacement is cancelled or rejected.
Live preview
Activate the field, then press one complete key combination.
Integration code·v0.4.1
InstallTerminal
npm install @a3s-lab/ui@0.4.1
<div class="field">
<label for="quick-search-shortcut">Open quick search</label>
<div
class="hotkey-input"
data-hotkey-value="Meta+K"
data-recording-placeholder="Press one key combination…"
data-recording-message="Press one complete key combination. Escape cancels."
data-modifier-message="Add a non-modifier key."
data-invalid-message="Enter one complete key combination at a time."
data-recording-announcement="Recording shortcut. Press a key combination, or Escape to cancel."
data-modifier-announcement="Modifier recorded. Add a non-modifier key."
data-recorded-announcement="Current shortcut"
data-empty-announcement="No shortcut assigned."
data-cancelled-announcement="Shortcut change cancelled."
data-cleared-announcement="Shortcut cleared."
data-rejected-announcement="That shortcut is unavailable. Choose another."
>
<input
id="quick-search-shortcut"
name="quickSearchShortcut"
aria-describedby="quick-search-shortcut-feedback"
placeholder="Press a shortcut"
readonly
autocomplete="off"
inputmode="none"
spellcheck="false"
/>
<span data-hotkey-preview></span>
<button
type="button"
class="btn"
data-size="icon-sm"
data-variant="ghost"
data-hotkey-clear
tabindex="-1"
aria-label="Clear shortcut and record another"
>
<svg aria-hidden="true" viewBox="0 0 20 20" fill="none">
<path
d="m6 6 8 8m0-8-8 8"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
</button>
<span
class="sr-only"
data-hotkey-status
aria-live="polite"
aria-atomic="true"
></span>
</div>
<p id="quick-search-shortcut-feedback" data-hotkey-feedback>
Activate the field, then press one complete key combination.
</p>
</div>
Use a visible label and associate the input with adjacent guidance. The input remains the form value; the keycaps are presentation only.
<div class="field"> <label for="quick-search-shortcut">Open quick search</label> <div class="hotkey-input" data-hotkey-value="Meta+K" data-recording-placeholder="Press one key combination…" data-recording-message="Press one complete key combination. Escape cancels." data-modifier-message="Add a non-modifier key." data-invalid-message="Enter one complete key combination at a time." data-recording-announcement="Recording shortcut. Press a key combination, or Escape to cancel." data-modifier-announcement="Modifier recorded. Add a non-modifier key." data-recorded-announcement="Current shortcut" data-empty-announcement="No shortcut assigned." data-cancelled-announcement="Shortcut change cancelled." data-cleared-announcement="Shortcut cleared." data-rejected-announcement="That shortcut is unavailable. Choose another." > <input id="quick-search-shortcut" name="quickSearchShortcut" aria-describedby="quick-search-shortcut-feedback" placeholder="Press a shortcut" readonly autocomplete="off" inputmode="none" spellcheck="false" /> <span data-hotkey-preview></span> <button type="button" class="btn" data-size="icon-sm" data-variant="ghost" data-hotkey-clear tabindex="-1" aria-label="Clear shortcut and record another" > <svg aria-hidden="true" viewBox="0 0 20 20" fill="none"> <path d="m6 6 8 8m0-8-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" /> </svg> </button> <span class="sr-only" data-hotkey-status aria-live="polite" aria-atomic="true" ></span> </div> <p id="quick-search-shortcut-feedback" data-hotkey-feedback> Activate the field, then press one complete key combination. </p></div>
For component-level loading, import Base, Button, Field, Hotkey Input, Kbd, the A3S visual style, the runtime, and the Hotkey Input controller. See Installation for CDN and other integrations.
The controller exposes getValue(), setValue(value, options), clear(options), start(options), stop(options), and refresh(). getValue() always returns the committed chord, including while an empty replacement draft is visible.
Canonical values contain zero or more modifiers in Control, Alt, Shift, Meta order and exactly one non-modifier key. Use Plus for the literal + key. Common API aliases such as Ctrl and Command are normalized; platform symbols never enter the stored value.
Use event.detail.reject(message) to reject duplicate, reserved, or permission-controlled assignments. This preserves the old chord, exits recording, and exposes the reason through both visible feedback and a3s:hotkey-rejected.
const input = document.querySelector(".hotkey-input");const assigned = new Map([["Control+Shift+X", "Command palette"]]);input.addEventListener("a3s:hotkey-before-change", (event) => { const command = assigned.get(event.detail.value); if (command) event.detail.reject(`Already assigned to ${command}.`);});input.addEventListener("a3s:hotkey-change", (event) => { saveShortcut(event.detail.value);});
Event
Detail
When it fires
a3s:hotkey-before-change
value, previousValue, source, reject(message)
Before a chord or clear action commits; cancel or reject here
Use Hotkey Input only for assigning one chord to a named command. Use a regular text field for shortcut search, a keycap for display-only hints, and a command list for browsing several existing assignments. The host owns the command registry, duplicate detection, operating-system reservations, permissions, persistence, and global listener registration.
Some operating-system or browser shortcuts never reach page JavaScript. Do not promise that every chord can be captured, and do not disable platform escape routes to force capture.
Ready, populated: the committed chord appears as platform-aware keycaps, while the normalized input value remains available to forms and assistive technology.
Empty: the clear action is hidden and adjacent guidance explains how to create the first assignment.
Recording: focusing the input or pressing Enter starts one bounded capture transaction. Modifier-only input keeps recording and asks for a base key.
Replacing: the clear action opens an empty draft. Escape or lost window focus restores the committed value; Tab, pointer exit, or an accepted chord commits the draft.
Rejected or invalid: the old committed value remains visible, focus stays on the input, and the specific conflict or format reason appears next to it. Press Enter to retry.
Disabled: the value remains readable, native disabled prevents capture, and adjacent text explains the policy or permission constraint.
Empty, invalid, and disabled
No shortcut is assigned. Activate the field to record one.
Already assigned to browser history. Press Enter to choose another.
This shortcut is managed by workspace policy.
<div class="grid w-full gap-6 md:grid-cols-2">
<div class="field">
<label for="hotkey-empty-en">Toggle inspector</label>
<div
class="hotkey-input"
data-recording-placeholder="Press one key combination…"
data-recording-message="Press one complete key combination. Escape cancels."
data-modifier-message="Add a non-modifier key."
data-recorded-announcement="Current shortcut"
data-empty-announcement="No shortcut assigned."
>
<input
id="hotkey-empty-en"
aria-describedby="hotkey-empty-feedback-en"
placeholder="Press a shortcut"
readonly
/>
<span data-hotkey-preview></span>
<button
type="button"
class="btn"
data-size="icon-sm"
data-variant="ghost"
data-hotkey-clear
tabindex="-1"
aria-label="Clear shortcut and record another"
>
<svg aria-hidden="true" viewBox="0 0 20 20" fill="none">
<path
d="m6 6 8 8m0-8-8 8"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
</button>
<span class="sr-only" data-hotkey-status aria-live="polite"></span>
</div>
<p id="hotkey-empty-feedback-en" data-hotkey-feedback>
No shortcut is assigned. Activate the field to record one.
</p>
</div>
<div class="field">
<label for="hotkey-invalid-en">Open command history</label>
<div
class="hotkey-input"
data-hotkey-value="Control+H"
aria-invalid="true"
data-state="invalid"
>
<input
id="hotkey-invalid-en"
aria-describedby="hotkey-invalid-feedback-en"
aria-invalid="true"
placeholder="Press a shortcut"
readonly
/>
<span data-hotkey-preview></span>
<button
type="button"
class="btn"
data-size="icon-sm"
data-variant="ghost"
data-hotkey-clear
tabindex="-1"
aria-label="Clear shortcut and record another"
>
<svg aria-hidden="true" viewBox="0 0 20 20" fill="none">
<path
d="m6 6 8 8m0-8-8 8"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
</button>
<span class="sr-only" data-hotkey-status aria-live="polite"></span>
</div>
<p id="hotkey-invalid-feedback-en" data-hotkey-feedback data-error>
Already assigned to browser history. Press Enter to choose another.
</p>
</div>
<div class="field">
<label for="hotkey-disabled-en">Open managed workspace</label>
<div
class="hotkey-input"
data-hotkey-value="Meta+W"
data-state="disabled"
>
<input
id="hotkey-disabled-en"
aria-describedby="hotkey-disabled-feedback-en"
placeholder="Press a shortcut"
readonly
disabled
/>
<span data-hotkey-preview></span>
<button
type="button"
class="btn"
data-size="icon-sm"
data-variant="ghost"
data-hotkey-clear
tabindex="-1"
aria-label="Clear shortcut and record another"
>
<svg aria-hidden="true" viewBox="0 0 20 20" fill="none">
<path
d="m6 6 8 8m0-8-8 8"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
</button>
<span class="sr-only" data-hotkey-status aria-live="polite"></span>
</div>
<p id="hotkey-disabled-feedback-en" data-hotkey-feedback>
This shortcut is managed by workspace policy.
</p>
</div>
</div>
Prefer a visible label; a placeholder is capture guidance, not the control name.
The input is the only default Tab stop. Delete or Backspace starts an empty replacement, Enter retries after commit or rejection, Tab leaves the complete control, and Escape cancels the active recording without blurring the input.
Modifier-only, cancellation, clearing, rejection, and committed values are announced through the bounded polite status region. Visible feedback remains present for people who do not receive live announcements.
Keycaps are aria-hidden because the read-only input already exposes the canonical chord. aria-keyshortcuts is intentionally omitted: the saved chord activates the host command, not the recorder itself.
KeyboardEvent.key keeps capture layout-aware. Dead, composing, and unidentified keys do not commit a partial chord; the literal plus key is encoded as Plus.
Window blur and document hiding cancel the active recording so browser switching cannot silently overwrite an assignment. Normal Tab or pointer exit commits an intentional empty replacement draft.
Native disabled is required for an unavailable input. Keep the committed value and an adjacent reason readable instead of relying on opacity or color alone.