Vize

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.

Open the interactive example

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
Scope review shortcuts to a chosen control: initial state
Initial state
Scope review shortcuts to a chosen control: state after the verified interactions
After the verified interactions

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.