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</p>`,
})
export class HotkeyDemo {
  constructor() {
    onKey(['Meta', 'K'], event => { 
      event.preventDefault();
      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.

Options

The OnKeyOptions extends WithInjector:

OptionTypeDefaultDescription
targetMaybeElementSignal<HTMLElement> | Window | DocumentwindowEvent target to listen on
eventName'keydown' | 'keyup''keydown'Keyboard event to listen for
passivebooleanfalseRegister the listener as passive
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(['Meta', 'P'])">Rebind</button>`,
})
export class ReactiveDemo {
  readonly hotkey = signal(['Meta', 'K']);

  constructor() {
    // Changing the signal re-binds the listener automatically
    onKey(this.hotkey, event => { 
      event.preventDefault();
      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';
  readonly passive?: boolean;
  readonly dedupe?: MaybeSignal<boolean>;
}

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

function onKey(
  key: KeyFilter,
  handler: (event: KeyboardEvent) => void,
  options?: OnKeyOptions,
): OnKeyRef;

function onKey(
  handler: (event: KeyboardEvent) => void,
  options?: OnKeyOptions,
): OnKeyRef;
Edit this page on GitHub Last updated: Aug 2, 2026, 19:58:46