OnKey

Listen for keyboard events matching a key filter: a single key, a key combination, a reactive signal of either, or a custom predicate.

Loading demo...

Usage

angular-ts
import { Component } from '@angular/core';
import { onKey } from '@signality/core';

@Component({
  template: `<p>Press ⌘K / Ctrl+K</p>`,
})
export class HotkeyDemo {
  constructor() {
    onKey.prevent(['Mod', 'K'], () => { 
      console.log('Command palette!');
    });
  }
}

Parameters

ParameterTypeDescription
keyKeyFilterKey filter: event.key string, key combination array, signal of either, or a custom predicate
handler(event: KeyboardEvent) => voidCallback invoked with the matching keyboard event
optionsOnKeyOptionsOptional configuration (see Options below)

The key parameter can be omitted — onKey(handler, options?) fires on every keyboard event.

Key filter semantics

  • String — matched against event.key. Single-character keys are compared case-insensitively: 'k' and 'K' are equivalent, so CapsLock does not affect the match.
  • Array — a key combination (all keys together): modifier keys ('Meta', 'Control', 'Alt', 'Shift', or the virtual 'Mod') plus at most one regular key. The match is exact — an extra pressed modifier prevents it, so ['Meta', 'K'] does not match Meta+Shift+K. The order of keys in the array does not matter. An array of only modifiers (e.g. ['Control', 'Shift']) fires on the keydown that completes the combination. Unlike a bare string, a single-key array is still exact: ['s'] does not fire while a modifier is held, whereas the string 's' also matches Ctrl+S.
  • Signal — a Signal<string | string[]> re-binds the listener whenever its value changes.
  • Predicate — full control: event => boolean. Use it for "any of" (OR) matching, which arrays intentionally do not provide: event => ['Escape', 'Enter'].includes(event.key).

Key aliases

Key names follow the canonical event.key values. Common aliases are accepted and resolved once when the filter is parsed (case-insensitively):

AliasResolved value
'Mod''Meta' () on Apple platforms, 'Control' everywhere else
'Ctrl''Control'
'Cmd', 'Command', 'Win''Meta'
'Option', 'Opt''Alt'
'Esc''Escape'
'Del''Delete'
'Return''Enter'
'Space'' ' (the literal space character)

The virtual 'Mod' modifier is the recommended way to declare cross-platform shortcuts: ['Mod', 'K'] fires on +K on macOS and Ctrl+K on Windows/Linux.

Modifiers

Behavior can be configured through chainable modifiers (combined in any order):

angular-ts
onKey.prevent(['Mod', 'K'], openCommandPalette);
onKey.capture.stop('Escape', closeTopmostLayer);
onKey.once('Enter', submit);

Available modifiers:

  • onKey.prevent(...) - calls event.preventDefault() on matching events
  • onKey.stop(...) - calls event.stopPropagation() on matching events
  • onKey.once(...) - destroys the listener after the first matching event
  • onKey.capture(...) - registers the underlying listener in the capture phase
  • onKey.passive(...) - registers the underlying listener as passive (replaces the deprecated passive option; incompatible with prevent)

Options

The OnKeyOptions extends WithInjector:

OptionTypeDefaultDescription
targetMaybeElementSignal<HTMLElement> | Window | DocumentwindowEvent target to listen on
eventName'keydown' | 'keyup''keydown'Keyboard event to listen for
passivebooleanfalseDeprecated — use the onKey.passive(...) modifier instead
dedupeMaybeSignal<boolean>falseIgnore repeated events while the key is held (event.repeat)
injectorInjector-Optional injector for DI context

Return Value

Returns an OnKeyRef with a destroy method to stop listening:

PropertyTypeDescription
destroy() => voidStops listening for keyboard events

Examples

Single key

angular-ts
import { Component, signal } from '@angular/core';
import { onKey } from '@signality/core';

@Component({
  template: `<dialog [open]="isOpen()">Press Escape to close</dialog>`,
})
export class EscapeDemo {
  readonly isOpen = signal(true);

  constructor() {
    onKey('Escape', () => this.isOpen.set(false)); 
  }
}

Reactive filter

angular-ts
import { Component, signal } from '@angular/core';
import { onKey } from '@signality/core';

@Component({
  template: `<button (click)="hotkey.set(['Mod', 'P'])">Rebind</button>`,
})
export class ReactiveDemo {
  readonly hotkey = signal(['Mod', 'K']);

  constructor() {
    // Changing the signal re-binds the listener automatically
    onKey.prevent(this.hotkey, () => { 
      console.log('Hotkey pressed!');
    });
  }
}

"Any of" matching with a predicate

angular-ts
import { Component } from '@angular/core';
import { onKey } from '@signality/core';

@Component({ template: `` })
export class SubmitOrDismissDemo {
  constructor() {
    onKey(
      event => ['Escape', 'Enter'].includes(event.key), 
      event => console.log('Dismiss or submit:', event.key)
    );
  }
}

Element target and dedupe

angular-ts
import { Component, viewChild, ElementRef } from '@angular/core';
import { onKey } from '@signality/core';

@Component({
  template: `<input #search placeholder="Search…" />`,
})
export class SearchDemo {
  readonly search = viewChild<ElementRef>('search');

  constructor() {
    onKey('ArrowDown', event => console.log('Next suggestion'), {
      target: this.search, 
      dedupe: true, 
    });
  }
}

SSR Compatibility

On the server, the utility returns a no-op ref with an empty destroy method.

Type Definitions

typescript
type KeyPredicate = (event: KeyboardEvent) => boolean;

type KeyFilter = MaybeSignal<string | string[]> | KeyPredicate;

interface OnKeyOptions extends WithInjector {
  readonly target?: MaybeElementSignal<HTMLElement> | Window | Document;
  readonly eventName?: 'keydown' | 'keyup';
  /** @deprecated Use the `onKey.passive(...)` modifier instead. */
  readonly passive?: boolean;
  readonly dedupe?: MaybeSignal<boolean>;
}

interface OnKeyRef {
  readonly destroy: () => void;
}

interface OnKeyFunction {
  (
    key: KeyFilter,
    handler: (event: KeyboardEvent) => void,
    options?: OnKeyOptions,
  ): OnKeyRef;

  (
    handler: (event: KeyboardEvent) => void,
    options?: OnKeyOptions,
  ): OnKeyRef;

  readonly capture: OnKeyFunction;
  readonly passive: OnKeyFunction;
  readonly once: OnKeyFunction;
  readonly stop: OnKeyFunction;
  readonly prevent: OnKeyFunction;
}

const onKey: OnKeyFunction;
Edit this page on GitHub Last updated: Aug 22, 2026, 20:11:09