on-key-stroke
Run a handler when matching keys are pressed.
| Package | @vizejs/composable/on-key-stroke |
| Own the source | vize lib pull composable:on-key-stroke |
| Runtime exports | onKeyStroke, onKeyDown, onKeyUp |
| Gzip budget | 3328 B |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/composable
Import the entry directly. Call lifecycle-bound utilities inside <script setup> so cleanup follows the component scope; consult the runtime contract for this entry's SSR and hydration behavior.
import { onKeyStroke, onKeyDown, onKeyUp } from "@vizejs/composable/on-key-stroke";
Try it
Scope review shortcuts to a chosen control
Navigate a bounded review checklist with element-scoped shortcuts while retaining native controls for touch and other keyboard users.
What to observe: Focus Primary review control and press J/K to move through the tips; ? toggles help. Holding J performs only the first action. Choose Secondary review control and verify that the shortcuts move there while the primary control no longer responds. Stop review shortcuts removes the listener; the native Previous/Next buttons still work.
target is a computed native-button ref, so the listener follows the selected element and detaches from the previous one. Key names are normalized; the handler interprets J/K and cancels only matched shortcuts. dedupe: true ignores repeated keydown events. The returned stop function permanently stops this registration, which the scope also owns. A null template target attaches nothing during SSR. Bounded tip state and native Previous/Next buttons belong to this example; the composable supplies only matching event delivery. Shortcuts are not registered on the document or window.
Compare the initial and interacted states


Copy the example
Save this complete SFC in your Vue project. Its public imports and source are the same ones executed in the preview.
<script setup lang="ts">
import { computed, ref, useId, useTemplateRef } from "vue";
import { onKeyStroke } from "@vizejs/composable/on-key-stroke";
const reviewId = useId();
const primary = useTemplateRef<HTMLButtonElement>("primary");
const secondary = useTemplateRef<HTMLButtonElement>("secondary");
const selectedTarget = ref("primary");
const target = computed(() =>
selectedTarget.value === "primary" ? primary.value : secondary.value,
);
const tips = ["Name the props", "Label the controls", "Handle the empty state"] as const;
const index = ref(0);
const currentTip = computed(() => tips[index.value]);
const helpVisible = ref(false);
const stopped = ref(false);
function moveBy(offset: number): void {
index.value = Math.max(0, Math.min(tips.length - 1, index.value + offset));
}
const stop = onKeyStroke(
["j", "k", "?"],
(event) => {
event.preventDefault();
if (event.key === "?") helpVisible.value = !helpVisible.value;
else moveBy(event.key.toLowerCase() === "j" ? 1 : -1);
},
{ target, dedupe: true },
);
function stopShortcuts(): void {
stop();
stopped.value = true;
}
</script>
<template>
<div class="composable-example">
<label :for="reviewId">Shortcut target</label>
<select :id="reviewId" v-model="selectedTarget" :disabled="stopped">
<option value="primary">Primary review control</option>
<option value="secondary">Secondary review control</option>
</select>
<div class="example-actions">
<button ref="primary" type="button" :aria-describedby="`${reviewId}-help`">
Primary review control
</button>
<button ref="secondary" type="button" :aria-describedby="`${reviewId}-help`">
Secondary review control
</button>
</div>
<p :id="`${reviewId}-help`">
Focus the selected control. J moves forward, K moves back, and ? toggles help. Holding a key
does not repeat the action. Other controls keep their normal keyboard behavior.
</p>
<output aria-live="polite">Tip {{ index + 1 }} of {{ tips.length }}: {{ currentTip }}</output>
<p v-if="helpVisible">
Review one component at a time. Name its props, label its controls, and test empty data.
</p>
<div class="example-actions">
<button type="button" :disabled="index === 0" @click="() => moveBy(-1)">
Previous review tip
</button>
<button type="button" :disabled="index === tips.length - 1" @click="() => moveBy(1)">
Next review tip
</button>
<button type="button" :disabled="stopped" @click="stopShortcuts">
Stop review shortcuts
</button>
</div>
<p>
{{
stopped
? "Shortcuts stopped. The native Previous/Next buttons still work."
: "Shortcuts follow only the selected element; native buttons also work on touch screens."
}}
</p>
</div>
</template>
Runtime contract
| Utility | Category | Stability | SSR | Hydration | Cleanup | Targets | Host globals | Uses |
|---|---|---|---|---|---|---|---|---|
onKeyStroke |
input | experimental | deterministic-fallback | stable | caller, reactive-scope | web, desktop | window |
normalizeKeyName, tryOnScopeDispose |
onKeyDown |
input | experimental | deterministic-fallback | stable | caller, reactive-scope | web, desktop | window |
onKeyStroke |
onKeyUp |
input | experimental | deterministic-fallback | stable | caller, reactive-scope | web, desktop | window |
onKeyStroke |
API
onKeyStroke
Run a handler when matching keys are pressed. key accepts a name, a list of names (normalized like combos, so "esc" matches Escape and "a" matches KeyA), a predicate, or true for every key. The listener follows the reactive target and is removed with the owning scope or the returned stop function; nothing is registered during server rendering.
function onKeyStroke( key: KeyFilter | true, handler: (event: KeyboardEvent) => void, options: OnKeyStrokeOptions = {}, ): () => void
onKeyDown
onKeyStroke for keydown.
function onKeyDown( key: KeyFilter | true, handler: (event: KeyboardEvent) => void, options: Omit<OnKeyStrokeOptions, "eventName"> = {}, ): () => void
onKeyUp
onKeyStroke for keyup.
function onKeyUp( key: KeyFilter | true, handler: (event: KeyboardEvent) => void, options: Omit<OnKeyStrokeOptions, "eventName"> = {}, ): () => void
Types
OnKeyStrokeOptions
Options for onKeyStroke.
| Member | Type | Description |
|---|---|---|
eventName? |
KeyStrokeEventName |
Keyboard event to listen for. |
target? |
MaybeRefOrGetter<EventTarget | null | undefined> |
Event target. |
passive? |
boolean |
Register a passive listener (the handler then cannot preventDefault()). |
dedupe? |
MaybeRefOrGetter<boolean> |
Ignore auto-repeated keydown events while a key is held. |