ngx-keys
v1.3.1
Published
A reactive Angular library for managing keyboard shortcuts with signals-based UI integration
Maintainers
Readme
ngx-keys
A lightweight, reactive Angular service for managing keyboard shortcuts with signals-based UI integration.
Features
- 🎯 Service-Focused: Clean, focused API without unnecessary UI components
- 📝 Declarative Directive: Optional directive for template-based shortcut registration
- ⚡ Reactive Signals: Track active/inactive shortcuts with Angular signals
- 🔧 UI-Agnostic: Build your own UI using the provided reactive signals
- 🌍 Cross-Platform: Automatic Mac/PC key display formatting
- 🔄 Dynamic Management: Add, remove, activate/deactivate shortcuts at runtime
- 📁 Group Management: Organize shortcuts into logical groups
- ⚙️ Configurable: Customize sequence timeout and other behavior via dependency injection
- 🔍 Smart Conflict Detection: Register multiple shortcuts with same keys when not simultaneously active
- 🪶 Lightweight: Zero dependencies, minimal bundle impact
Installation
npm install ngx-keysQuick Start
Register and Display Shortcuts
[!NOTE] For related shortcuts, use groups for easier management (See group management section).
import { Component, DestroyRef, inject } from '@angular/core';
import { KeyboardShortcuts } from 'ngx-keys';
@Component({
template: `
<h3>My App</h3>
<p>Last action: {{ lastAction }}</p>
<h4>Available Shortcuts:</h4>
@for (shortcut of activeShortcuts(); track shortcut.id) {
<div>
<kbd>{{ shortcut.keys }}</kbd> - {{ shortcut.description }}
</div>
}
`
})
export class MyComponent {
private readonly keyboardService = inject(KeyboardShortcuts);
private readonly destroyRef = inject(DestroyRef);
protected lastAction = 'Try pressing Ctrl+S or Ctrl+H';
protected readonly activeShortcuts = () => this.keyboardService.shortcutsUI$().active;
constructor() {
// Register individual shortcuts (automatically activated)
this.keyboardService.register({
id: 'save',
keys: ['ctrl', 's'],
macKeys: ['meta', 's'],
action: () => this.save(),
description: 'Save document'
});
this.keyboardService.register({
id: 'help',
keys: ['ctrl', 'h'],
macKeys: ['meta', 'h'],
action: () => this.showHelp(),
description: 'Show help'
});
// Clean up on component destroy
this.destroyRef.onDestroy(() => {
this.keyboardService.unregister('save');
this.keyboardService.unregister('help');
});
}
private save() {
this.lastAction = 'Document saved!';
}
private showHelp() {
this.lastAction = 'Help displayed!';
}
}Using the Directive (Declarative Approach)
For a more declarative approach, use the ngxKeys directive directly on your elements:
import { Component } from '@angular/core';
import { KeyboardShortcutDirective } from 'ngx-keys';
@Component({
standalone: true,
imports: [KeyboardShortcutDirective],
template: `
<h3>My App</h3>
<p>Last action: {{ lastAction }}</p>
<!-- Directive automatically registers and unregisters shortcuts -->
<button
ngxKeys
keys="ctrl,s"
macKeys="cmd,s"
description="Save document"
(click)="save()">
Save
</button>
<button
ngxKeys
keys="ctrl,h"
macKeys="cmd,h"
description="Show help"
(click)="showHelp()">
Help
</button>
<!-- Multi-step shortcuts work too -->
<button
ngxKeys
[steps]="[['ctrl', 'k'], ['ctrl', 'p']]"
[macSteps]="[['cmd', 'k'], ['cmd', 'p']]"
description="Open command palette (Ctrl+K then Ctrl+P)"
(click)="openCommandPalette()">
Command Palette
</button>
<!-- Use custom action instead of click -->
<div
ngxKeys
keys="?"
description="Show help overlay"
[action]="showHelpOverlay">
Press ? for help
</div>
`
})
export class MyComponent {
protected lastAction = 'Try pressing Ctrl+S or Ctrl+H';
protected readonly showHelpOverlay = () => {
this.lastAction = 'Help overlay displayed!';
};
protected save() {
this.lastAction = 'Document saved!';
}
protected showHelp() {
this.lastAction = 'Help displayed!';
}
protected openCommandPalette() {
this.lastAction = 'Command palette opened!';
}
}[!TIP] The directive automatically:
- Registers shortcuts when initialized
- Triggers click events on the host element (or executes a custom action)
- Unregisters shortcuts when destroyed
- Adds a
data-keyboard-shortcutattribute for styling/testing
Directive Inputs:
| Input | Type | Description |
|-------|------|-------------|
| keys | string | Comma-separated keys for single-step shortcut (e.g., "ctrl,s") |
| macKeys | string | Comma-separated keys for Mac (e.g., "cmd,s") |
| steps | string[][] | Multi-step sequence (e.g., [['ctrl', 'k'], ['ctrl', 'd']]) |
| macSteps | string[][] | Multi-step sequence for Mac |
| description | string | Required. Description of what the shortcut does |
| action | () => void | Optional custom action. If not provided, triggers click on host element |
| shortcutId | string | Optional custom ID. If not provided, generates unique ID |
Directive Outputs:
| Output | Type | Description |
|--------|------|-------------|
| triggered | void | Emitted when the keyboard shortcut is triggered |
When to use the directive vs. the service:
- Use the directive when shortcuts are tied to specific UI elements and their lifecycle
- Use the service when you need programmatic control, dynamic shortcuts, or shortcuts not tied to elements
Explore the Demo
Want to see ngx-keys in action? Check out our comprehensive demo application with:
| Component | Purpose | Key Features | | ---------------------------------------------------------------------------------- | -------------------------- | --------------------------------------------- | | App Component | Global shortcuts | Single & group registration, cleanup patterns | | Home Component | Reactive UI | Real-time status display, toggle controls | | Feature Component | Route-specific shortcuts | Scoped shortcuts, lifecycle management | | Directive Demo | Declarative shortcuts | Directive usage, automatic lifecycle management | | Customize Component | Dynamic shortcut recording | Real-time key capture, shortcut customization |
Run the demo:
git clone https://github.com/mrivasperez/ngx-keys
cd ngx-keys
npm install
npm startKey Concepts
Automatic Activation
[!IMPORTANT] When you register shortcuts using
register()orregisterGroup(), they are automatically activated and ready to use immediately. You don't need to callactivate()unless you've previously deactivated them.
// This shortcut is immediately active after registration
this.keyboardService.register({
id: 'save',
keys: ['ctrl', 's'],
macKeys: ['meta', 's'],
action: () => this.save(),
description: 'Save document'
});Smart Conflict Detection
[!IMPORTANT] Conflicts are only checked among active shortcuts, not all registered shortcuts.
ngx-keys allows registering multiple shortcuts with the same key combination, as long as they're not simultaneously active. This enables powerful patterns:
- Context-specific shortcuts: Same keys for different UI contexts
- Alternative shortcuts: Multiple ways to trigger the same action
- Feature toggles: Same keys for different modes
// Basic conflict handling
this.keyboardService.register(shortcut1); // Active by default
this.keyboardService.deactivate('shortcut1');
this.keyboardService.register(shortcut2); // Same keys, but shortcut1 is inactive ✅
// This would fail - conflicts with active shortcut2
// this.keyboardService.activate('shortcut1'); // ❌ Throws errorGroup Management
Organize related shortcuts into groups for easier management:
const editorShortcuts = [
{
id: 'bold',
keys: ['ctrl', 'b'],
macKeys: ['meta', 'b'],
action: () => this.makeBold(),
description: 'Make text bold'
},
{
id: 'italic',
keys: ['ctrl', 'i'],
macKeys: ['meta', 'i'],
action: () => this.makeItalic(),
description: 'Make text italic'
}
];
// Register all shortcuts in the group
this.keyboardService.registerGroup('editor', editorShortcuts);
// Control the entire group
this.keyboardService.deactivateGroup('editor'); // Disable all editor shortcuts
this.keyboardService.activateGroup('editor'); // Re-enable all editor shortcutsMulti-step (sequence) shortcuts
In addition to single-step shortcuts using keys / macKeys, ngx-keys supports ordered multi-step sequences using steps and macSteps on the KeyboardShortcut object. Each element in steps is itself an array of key tokens that must be pressed together for that step.
Example: register a sequence that requires Ctrl+K followed by Ctrl+D:
this.keyboardService.register({
id: 'format-document-seq',
steps: [['ctrl', 'k'], ['ctrl', 'd']],
macSteps: [['meta', 'k'], ['meta', 'd']],
action: () => this.formatDocument(),
description: 'Format document (Ctrl+K then Ctrl+D)'
});Configuring Sequence Timeout
By default, multi-step shortcuts have no timeout - users can take as long as they need between steps. You can configure a timeout globally using a provider function:
import { ApplicationConfig } from '@angular/core';
import { provideKeyboardShortcutsConfig } from 'ngx-keys';
export const appConfig: ApplicationConfig = {
providers: [
provideKeyboardShortcutsConfig({ sequenceTimeoutMs: 2000 }) // 2 seconds
]
};Alternatively, you can use the injection token directly:
import { ApplicationConfig } from '@angular/core';
import { KEYBOARD_SHORTCUTS_CONFIG } from 'ngx-keys';
export const appConfig: ApplicationConfig = {
providers: [
{
provide: KEYBOARD_SHORTCUTS_CONFIG,
useValue: { sequenceTimeoutMs: 2000 } // 2 seconds
}
]
};Important behavior notes
- Sequence timeout: Steps must be entered within the configured timeout (default 2000ms) or the sequence is cleared.
- Order-sensitive: Steps are order-sensitive.
steps: [['ctrl','k'], ['s']]is different fromsteps: [['s'], ['ctrl','k']].
Use the activate() and deactivate() methods for dynamic control after registration:
// Temporarily disable a shortcut
this.keyboardService.deactivate('save');
// Re-enable it later
this.keyboardService.activate('save');Advanced Usage
Type-Safe Action Functions
For better type safety and code reusability, use the exported Action type when defining action functions:
import { Component, inject } from '@angular/core';
import { KeyboardShortcuts, Action } from 'ngx-keys';
export class MyComponent {
private readonly keyboardService = inject(KeyboardShortcuts);
// Define reusable, type-safe action functions
private readonly saveAction: Action = () => {
console.log('Saving document...');
this.performSave();
};
private readonly undoAction: Action = () => {
console.log('Undoing...');
this.performUndo();
};
constructor() {
// Use the typed actions in shortcuts
this.keyboardService.register({
id: 'save',
keys: ['ctrl', 's'],
macKeys: ['meta', 's'],
action: this.saveAction,
description: 'Save document'
});
this.keyboardService.register({
id: 'undo',
keys: ['ctrl', 'z'],
macKeys: ['meta', 'z'],
action: this.undoAction,
description: 'Undo last action'
});
}
private performSave() { /* implementation */ }
private performUndo() { /* implementation */ }
}Context-Specific Shortcuts
Register different actions for the same keys in different UI contexts:
// Modal context
this.keyboardService.register({
id: 'modal-escape',
keys: ['escape'],
action: () => this.closeModal(),
description: 'Close modal'
});
// Initially deactivate since modal isn't shown
this.keyboardService.deactivate('modal-escape');
// Editor context
this.keyboardService.register({
id: 'editor-escape',
keys: ['escape'], // Same key, different context
action: () => this.exitEditMode(),
description: 'Exit edit mode'
});
// Switch contexts dynamically
showModal() {
this.keyboardService.deactivate('editor-escape');
this.keyboardService.activate('modal-escape');
}
hideModal() {
this.keyboardService.deactivate('modal-escape');
this.keyboardService.activate('editor-escape');
}Alternative Shortcuts
Provide multiple ways to trigger the same functionality:
// Primary shortcut
this.keyboardService.register({
id: 'help-f1',
keys: ['f1'],
action: () => this.showHelp(),
description: 'Show help (F1)'
});
// Alternative shortcut - different keys, same action
this.keyboardService.register({
id: 'help-ctrl-h',
keys: ['ctrl', 'h'],
action: () => this.showHelp(), // Same action
description: 'Show help (Ctrl+H)'
});
// Both are active simultaneously since they don't conflictFeature Toggles
Switch between different modes that use the same keys:
// Design mode
this.keyboardService.register({
id: 'design-mode-space',
keys: ['space'],
action: () => this.toggleDesignElement(),
description: 'Toggle design element'
});
// Play mode (same key, different action)
this.keyboardService.register({
id: 'play-mode-space',
keys: ['space'],
action: () => this.pausePlayback(),
description: 'Pause/resume playback'
});
// Initially deactivate play mode
this.keyboardService.deactivate('play-mode-space');
// Switch modes
switchToPlayMode() {
this.keyboardService.deactivate('design-mode-space');
this.keyboardService.activate('play-mode-space');
}
switchToDesignMode() {
this.keyboardService.deactivate('play-mode-space');
this.keyboardService.activate('design-mode-space');
}Advanced Group Patterns
Use groups for complex activation/deactivation scenarios:
// Create context-specific groups
const modalShortcuts = [
{ id: 'modal-close', keys: ['escape'], action: () => this.closeModal(), description: 'Close modal' },
{ id: 'modal-confirm', keys: ['enter'], action: () => this.confirmModal(), description: 'Confirm' }
];
const editorShortcuts = [
{ id: 'editor-save', keys: ['ctrl', 's'], action: () => this.save(), description: 'Save' },
{ id: 'editor-undo', keys: ['ctrl', 'z'], action: () => this.undo(), description: 'Undo' }
];
// Register both groups
this.keyboardService.registerGroup('modal', modalShortcuts);
this.keyboardService.registerGroup('editor', editorShortcuts);
// Initially only editor is active
this.keyboardService.deactivateGroup('modal');
// Switch contexts
showModal() {
this.keyboardService.deactivateGroup('editor');
this.keyboardService.activateGroup('modal');
}
hideModal() {
this.keyboardService.deactivateGroup('modal');
this.keyboardService.activateGroup('editor');
}Conflict Detection Rules
- Registration: Only checks conflicts with currently active shortcuts
- Activation: Throws error if activating would conflict with other active shortcuts
- Groups: Same rules apply - groups can contain conflicting shortcuts as long as they're not simultaneously active
// ✅ This works - shortcuts with same keys but only one active at a time
this.keyboardService.register(shortcut1); // Active by default
this.keyboardService.deactivate('shortcut1');
this.keyboardService.register(shortcut2); // Same keys, but shortcut1 is inactive
// ❌ This fails - trying to activate would create conflict
this.keyboardService.activate('shortcut1'); // Throws error - conflicts with active shortcut2API Reference
KeyboardShortcuts Service
Methods
Registration Methods:
[!TIP] Conflicts are only checked among active shortcuts, not all registered shortcuts.
register(shortcut: KeyboardShortcut)- Register and automatically activate a single shortcut Throws error on conflicts with active shortcuts onlyregisterGroup(groupId: string, shortcuts: KeyboardShortcut[])- Register and automatically activate a group of shortcuts Throws error on conflicts with active shortcuts onlyregisterMany(shortcuts: KeyboardShortcut[])- Register multiple shortcuts in a single batch update
Unregistration Methods:
[!NOTE]
unregister()automatically removes shortcuts from all groups they belong to.
unregister(shortcutId: string)- Remove a shortcut and its group associations Throws error if not foundunregisterGroup(groupId: string)- Remove a group and all its shortcuts Throws error if not foundunregisterMany(ids: string[])- Unregister multiple shortcuts in a single batch updateunregisterGroups(ids: string[])- Unregister multiple groups in a single batch updateclearAll()- Remove all shortcuts, groups, and filters (nuclear reset)
Activation Methods:
activate(shortcutId: string)- Activate a shortcut Throws error if not registered or would create conflictsdeactivate(shortcutId: string)- Deactivate a shortcut Throws error if not registeredactivateGroup(groupId: string)- Activate all shortcuts in a group Throws error if not found or would create conflictsdeactivateGroup(groupId: string)- Deactivate all shortcuts in a group Throws error if not found
Filter Methods:
addFilter(name: string, filter: Function)- Add a named global filterremoveFilter(name: string)- Remove a named global filterclearFilters()- Remove all global filtershasFilter(name: string): boolean- Check if a filter existsgetFilter(name: string)- Get a filter function by namegetFilterNames(): string[]- Get all filter namesremoveGroupFilter(groupId: string)- Remove filter from a groupremoveShortcutFilter(shortcutId: string)- Remove filter from a shortcutclearAllGroupFilters()- Remove all group filtersclearAllShortcutFilters()- Remove all shortcut filtershasGroupFilter(groupId: string): boolean- Check if group has a filterhasShortcutFilter(shortcutId: string): boolean- Check if shortcut has a filter
Query Methods:
isActive(shortcutId: string): boolean- Check if a shortcut is activeisRegistered(shortcutId: string): boolean- Check if a shortcut is registeredisGroupActive(groupId: string): boolean- Check if a group is activeisGroupRegistered(groupId: string): boolean- Check if a group is registeredgetShortcuts(): ReadonlyMap<string, KeyboardShortcut>- Get all registered shortcutsgetGroups(): ReadonlyMap<string, KeyboardShortcutGroup>- Get all registered groupsgetGroupShortcuts(groupId: string): KeyboardShortcut[]- Get all shortcuts in a specific group
Utility Methods:
formatShortcutForUI(shortcut: KeyboardShortcut): KeyboardShortcutUI- Format a shortcut for displaybatchUpdate(operations: () => void): void- Batch multiple operations to reduce signal updates
Reactive Signals
The service provides reactive signals for UI integration:
// Primary signal containing all shortcut state
shortcuts$: Signal<{
active: KeyboardShortcut[];
inactive: KeyboardShortcut[];
all: KeyboardShortcut[];
groups: {
active: string[];
inactive: string[];
};
}>
// Pre-formatted UI signal for easy display
shortcutsUI$: Signal<{
active: ShortcutUI[];
inactive: ShortcutUI[];
all: ShortcutUI[];
}>ShortcutUI Interface:
interface KeyboardShortcutUI {
id: string; // Shortcut identifier
keys: string; // Formatted PC/Linux keys (e.g., "Ctrl+S")
macKeys: string; // Formatted Mac keys (e.g., "⌘+S")
description: string; // Human-readable description
}KeyboardShortcut Interface
// Type for shortcut action functions
type Action = () => void;
interface KeyboardShortcut {
id: string; // Unique identifier
// Single-step combinations (existing API)
keys?: string[]; // Key combination for PC/Linux (e.g., ['ctrl', 's'])
macKeys?: string[]; // Key combination for Mac (e.g., ['meta', 's'])
// Multi-step sequences (new)
// Each step is an array of keys pressed together. Example: steps: [['ctrl','k'], ['s']]
steps?: string[][];
macSteps?: string[][];
action: Action; // Function to execute
description: string; // Human-readable description
}KeyboardShortcutGroup Interface
interface KeyboardShortcutGroup {
id: string; // Unique group identifier
shortcuts: KeyboardShortcut[]; // Array of shortcuts in this group
active: boolean; // Whether the group is currently active
}KeyboardShortcutDirective
A declarative directive for registering keyboard shortcuts directly in templates.
Selector
[ngxKeys]Inputs
| Input | Type | Required | Description |
|-------|------|----------|-------------|
| keys | string | No* | Comma-separated keys for single-step shortcut (e.g., "ctrl,s") |
| macKeys | string | No | Comma-separated keys for Mac (e.g., "cmd,s") |
| steps | string[][] | No* | Multi-step sequence. Each inner array is one step (e.g., [['ctrl', 'k'], ['ctrl', 'd']]) |
| macSteps | string[][] | No | Multi-step sequence for Mac |
| description | string | Yes | Human-readable description of what the shortcut does |
| action | () => void | No | Custom action to execute. If not provided, triggers click on host element |
| shortcutId | string | No | Custom ID for the shortcut. If not provided, generates a unique ID |
* Either keys/macKeys OR steps/macSteps must be provided (not both)
Outputs
| Output | Type | Description |
|--------|------|-------------|
| triggered | void | Emitted when the keyboard shortcut is triggered (in addition to the action) |
Host Bindings
The directive adds the following to the host element:
data-keyboard-shortcutattribute with the shortcut ID (useful for styling or testing)
Behavior
- Registration: Automatically registers the shortcut when the directive initializes
- Default Action: Triggers a click event on the host element (unless custom
actionis provided) - Cleanup: Automatically unregisters the shortcut when the directive is destroyed
- Error Handling: Throws errors for invalid input combinations or registration failures
Usage Examples
Basic button with click trigger:
<button
ngxKeys
keys="ctrl,s"
macKeys="cmd,s"
description="Save document"
(click)="save()">
Save
</button>Multi-step shortcut:
<button
ngxKeys
[steps]="[['ctrl', 'k'], ['ctrl', 'd']]"
[macSteps]="[['cmd', 'k'], ['cmd', 'd']]"
description="Format document (Ctrl+K then Ctrl+D)"
(click)="format()">
Format
</button>Custom action on non-interactive element:
<div
ngxKeys
keys="?"
description="Show help"
[action]="showHelp">
Press ? for help
</div>Listen to triggered event:
<button
ngxKeys
keys="ctrl,s"
description="Save document"
(triggered)="onShortcutTriggered()"
(click)="save()">
Save
</button>Custom shortcut ID:
<button
ngxKeys
keys="ctrl,s"
description="Save document"
shortcutId="my-custom-save-shortcut"
(click)="save()">
Save
</button>Key Mapping Reference
Modifier Keys
| PC/Linux | Mac | Description |
| -------- | ------- | ------------------- |
| ctrl | meta | Control/Command key |
| alt | alt | Alt/Option key |
| shift | shift | Shift key |
Special Keys
| Key | Value |
| ------------- | ------------------------------------------------- |
| Function keys | f1, f2, f3, ... f12 |
| Arrow keys | arrowup, arrowdown, arrowleft, arrowright |
| Navigation | home, end, pageup, pagedown |
| Editing | insert, delete, backspace |
| Other | escape, tab, enter, space |
Browser Conflicts Warning
[!WARNING] Some key combinations conflict with browser defaults. Use these with caution:
High-Risk Combinations (avoid these)
Ctrl+N/⌘+N- New tab/windowCtrl+T/⌘+T- New tabCtrl+W/⌘+W- Close tabCtrl+R/⌘+R- Reload pageCtrl+L/⌘+L- Focus address barCtrl+D/⌘+D- Bookmark page
Safer Alternatives
[!TIP] Always test your shortcuts across different browsers and operating systems. Consider providing alternative key combinations or allow users to customize shortcuts.
- Function keys:
F1,F2,F3, etc. - Custom combinations:
Ctrl+Shift+S,Alt+Enter - Arrow keys with modifiers:
Ctrl+ArrowUp - Application-specific:
Ctrl+K,Ctrl+P(if not conflicting)
Advanced Usage
[!TIP] Check out our demo application for full implementations of all patterns shown below.
Reactive UI Integration
import { Component, inject } from '@angular/core';
import { KeyboardShortcuts } from 'ngx-keys';
@Component({
template: `
<section>
<h3>Active Shortcuts ({{ activeShortcuts().length }})</h3>
@for (shortcut of activeShortcuts(); track shortcut.id) {
<div>
<kbd>{{ shortcut.keys }}</kbd> - {{ shortcut.description }}
</div>
}
</section>
<section>
<h3>Active Groups</h3>
@for (groupId of activeGroups(); track groupId) {
<div>{{ groupId }}</div>
}
</section>
`
})
export class ShortcutsDisplayComponent {
private readonly keyboardService = inject(KeyboardShortcuts);
// Access formatted shortcuts for display
protected readonly activeShortcuts = () => this.keyboardService.shortcutsUI$().active;
protected readonly inactiveShortcuts = () => this.keyboardService.shortcutsUI$().inactive;
protected readonly allShortcuts = () => this.keyboardService.shortcutsUI$().all;
// Access group information
protected readonly activeGroups = () => this.keyboardService.shortcuts$().groups.active;
protected readonly inactiveGroups = () => this.keyboardService.shortcuts$().groups.inactive;
}Group Management
[!NOTE] Live Example: See this pattern in action in feature.component.ts
import { Component, DestroyRef, inject } from '@angular/core';
import { KeyboardShortcuts, KeyboardShortcut } from 'ngx-keys';
export class FeatureComponent {
private readonly keyboardService = inject(KeyboardShortcuts);
private readonly destroyRef = inject(DestroyRef);
constructor() {
const shortcuts: KeyboardShortcut[] = [
{
id: 'cut',
keys: ['ctrl', 'x'],
macKeys: ['meta', 'x'],
action: () => this.cut(),
description: 'Cut selection'
},
{
id: 'copy',
keys: ['ctrl', 'c'],
macKeys: ['meta', 'c'],
action: () => this.copy(),
description: 'Copy selection'
}
];
// Group is automatically activated when registered
this.keyboardService.registerGroup('edit-shortcuts', shortcuts);
// Setup cleanup on destroy
this.destroyRef.onDestroy(() => {
this.keyboardService.unregisterGroup('edit-shortcuts');
});
}
toggleEditMode(enabled: boolean) {
if (enabled) {
this.keyboardService.activateGroup('edit-shortcuts');
} else {
this.keyboardService.deactivateGroup('edit-shortcuts');
}
}
private cut() { /* implementation */ }
private copy() { /* implementation */ }
}Automatic unregistering
register and registerGroup have the optional parameter: activeUntil.
The activeUntil parameter allows you to connect the shortcut to the wrappers lifecycle or logic in general.
activeUntil supports three types:
'destruct': the shortcut injects the parentsDestroyRefand unregisters once the component destructsDestroyRef: DestroyRef which should trigger the destruction of the shortcutObservable<unknown>: an Observable which will unregister the shortcut when triggered
Example: 'destruct'
Shortcuts defined by this component will only be listening during the lifecycle of the component. Shortcuts are registered on construction and are automatically unregistered on destruction.
export class Component {
constructor() {
const keyboardService = inject(KeyboardShortcuts)
keyboardService.register({
// ...
activeUntil: 'destruct', // alternatively: inject(DestroyRef)
});
keyboardService.registerGroup(
'shortcuts',
[/* ... */],
'destruct', // alternatively: inject(DestroyRef)
);
}
}Example: Observable
const shortcutTTL = new Subject<void>();
keyboardService.register({
// ...
activeUntil: shortcutTTL,
});
keyboardService.registerGroup(
'shortcuts',
[/* ... */],
shortcutTTL,
);
// Shortcuts are listening...
shortcutTTL.next();
// Shortcuts are unregisteredBatch Operations
For better performance when making multiple changes, use the batchUpdate method.
import { Component, inject } from '@angular/core';
import { KeyboardShortcuts } from 'ngx-keys';
export class BatchUpdateComponent {
private readonly keyboardService = inject(KeyboardShortcuts);
constructor() {
this.setupMultipleShortcuts();
}
private setupMultipleShortcuts() {
// Batch multiple operations to reduce signal updates
// Note: Shortcuts are automatically activated when registered
this.keyboardService.batchUpdate(() => {
this.keyboardService.register({
id: 'action1',
keys: ['ctrl', '1'],
macKeys: ['meta', '1'],
action: () => this.action1(),
description: 'Action 1'
});
this.keyboardService.register({
id: 'action2',
keys: ['ctrl', '2'],
macKeys: ['meta', '2'],
action: () => this.action2(),
description: 'Action 2'
});
});
}
private action1() { /* implementation */ }
private action2() { /* implementation */ }
}Bulk Registration and Unregistration
Register or unregister multiple shortcuts efficiently:
import { Component, inject } from '@angular/core';
import { KeyboardShortcuts } from 'ngx-keys';
export class MyComponent {
private readonly keyboardService = inject(KeyboardShortcuts);
setupToolbarShortcuts() {
// Register multiple shortcuts at once
this.keyboardService.registerMany([
{
id: 'save',
keys: ['ctrl', 's'],
macKeys: ['meta', 's'],
action: () => this.save(),
description: 'Save document'
},
{
id: 'save-as',
keys: ['ctrl', 'shift', 's'],
macKeys: ['meta', 'shift', 's'],
action: () => this.saveAs(),
description: 'Save as...'
},
{
id: 'print',
keys: ['ctrl', 'p'],
macKeys: ['meta', 'p'],
action: () => this.print(),
description: 'Print document'
}
]);
}
cleanup() {
// Unregister multiple shortcuts at once
this.keyboardService.unregisterMany(['save', 'save-as', 'print']);
// Or unregister entire groups
this.keyboardService.unregisterGroups(['toolbar', 'menu']);
// Or clear everything
this.keyboardService.clearAll();
}
private save() { /* implementation */ }
private saveAs() { /* implementation */ }
private print() { /* implementation */ }
}Managing Group Shortcuts
Remove individual shortcuts from groups and query group contents:
import { Component, inject } from '@angular/core';
import { KeyboardShortcuts } from 'ngx-keys';
export class MyComponent {
private readonly keyboardService = inject(KeyboardShortcuts);
manageEditorGroup() {
// Check if a shortcut is registered
if (this.keyboardService.isRegistered('bold')) {
// Remove a shortcut - automatically removes it from all groups
this.keyboardService.unregister('bold');
}
// Remove multiple shortcuts (automatically removes from groups)
this.keyboardService.unregisterMany(['bold', 'italic', 'underline']);
// Get all shortcuts in a group
const editorShortcuts = this.keyboardService.getGroupShortcuts('editor');
console.log('Remaining editor shortcuts:', editorShortcuts);
// Or remove the entire group and all its shortcuts
this.keyboardService.unregisterGroup('editor');
}
}Filter Management
Remove filters when no longer needed:
import { Component, inject } from '@angular/core';
import { KeyboardShortcuts } from 'ngx-keys';
export class MyComponent {
private readonly keyboardService = inject(KeyboardShortcuts);
setupModalFilters() {
// Add filters for modal context
this.keyboardService.addGroupFilter('navigation', () => this.isModalOpen);
this.keyboardService.addShortcutFilter('global-search', () => this.isModalOpen);
}
cleanupModalFilters() {
// Check if filters exist
if (this.keyboardService.hasGroupFilter('navigation')) {
// Remove specific group filter
this.keyboardService.removeGroupFilter('navigation');
}
if (this.keyboardService.hasShortcutFilter('global-search')) {
// Remove specific shortcut filter
this.keyboardService.removeShortcutFilter('global-search');
}
// Or clear all filters at once
this.keyboardService.clearAllGroupFilters();
this.keyboardService.clearAllShortcutFilters();
}
private isModalOpen = false;
}Checking Status
[!NOTE] See status checking in home.component.ts
import { Component, inject } from '@angular/core';
import { KeyboardShortcuts } from 'ngx-keys';
export class MyComponent {
private readonly keyboardService = inject(KeyboardShortcuts);
checkAndActivate() {
// Check before performing operations
if (this.keyboardService.isRegistered('my-shortcut')) {
this.keyboardService.activate('my-shortcut');
}
if (this.keyboardService.isGroupRegistered('my-group')) {
this.keyboardService.activateGroup('my-group');
}
}
}Chords (multiple non-modifier keys)
- ngx-keys supports chords composed of multiple non-modifier keys pressed simultaneously (for example
C + A). - When multiple non-modifier keys are physically held down at the same time, the service uses the set of currently pressed keys plus any modifier flags to match registered shortcuts.
- Example: register a chord with
keys: ['c','a']and pressing and holdingcthen pressingawill trigger the shortcut. - Note: Browsers deliver separate keydown events for each physical key; the library maintains a Set of currently-down keys via
keydown/keyuplisteners to enable chords. This approach attempts to be robust but can be affected by browser focus changes — ensure tests in your target browsers.
Example registration:
this.keyboardService.register({
id: 'chord-ca',
keys: ['c', 'a'],
macKeys: ['c', 'a'],
action: () => console.log('Chord C+A executed'),
description: 'Demo chord'
});Event Filtering
You can configure which keyboard events should be processed by setting a filter function. This is useful for ignoring shortcuts when users are typing in input fields, text areas, or other form elements.
[!NOTE] No Default Filtering: ngx-keys processes ALL keyboard events by default. This gives you maximum flexibility - some apps want shortcuts to work everywhere, others want to exclude form inputs. You decide!
Named filters (recommended)
For efficiency and control, prefer named global filters. You can toggle them on/off without replacing others, and ngx-keys evaluates them only once per keydown event (fast path), short‑circuiting further work when blocked.
// Add named filters
keyboardService.addFilter('forms', (event) => {
const t = event.target as HTMLElement | null;
const tag = t?.tagName?.toLowerCase();
return !(['input', 'textarea', 'select'].includes(tag ?? '')) && !t?.isContentEditable;
});
keyboardService.addFilter('modal-scope', (event) => {
const t = event.target as HTMLElement | null;
return !!t?.closest('.modal');
});
// Remove/toggle when context changes
keyboardService.removeFilter('modal-scope');
// Inspect and manage
keyboardService.getFilterNames(); // ['forms']
keyboardService.clearFilters(); // remove allimport { Component, inject } from '@angular/core';
import { KeyboardShortcuts, KeyboardShortcutFilter } from 'ngx-keys';
export class FilterExampleComponent {
private readonly keyboardService = inject(KeyboardShortcuts);
constructor() {
// Set up shortcuts
this.keyboardService.register({
id: 'save',
keys: ['ctrl', 's'],
macKeys: ['meta', 's'],
action: () => this.save(),
description: 'Save document'
});
// Configure filtering to ignore form elements
this.setupInputFiltering();
}
private setupInputFiltering() {
const inputFilter: KeyboardShortcutFilter = (event) => {
const target = event.target as HTMLElement;
const tagName = target?.tagName?.toLowerCase();
return !['input', 'textarea', 'select'].includes(tagName) && !target?.isContentEditable;
};
// Use named filter for toggling
this.keyboardService.addFilter('forms', inputFilter);
}
private save() {
console.log('Document saved!');
}
}Common Filter Patterns
Ignore form elements:
const formFilter: KeyboardShortcutFilter = (event) => {
const target = event.target as HTMLElement;
const tagName = target?.tagName?.toLowerCase();
return !['input', 'textarea', 'select'].includes(tagName) && !target?.isContentEditable;
};
keyboardService.addFilter('forms', formFilter);Ignore elements with specific attributes:
const attributeFilter: KeyboardShortcutFilter = (event) => {
const target = event.target as HTMLElement;
return !target?.hasAttribute('data-no-shortcuts');
};
keyboardService.addFilter('no-shortcuts-attr', attributeFilter);Complex conditional filtering:
const conditionalFilter: KeyboardShortcutFilter = (event) => {
const target = event.target as HTMLElement;
// Allow shortcuts in code editors (even though they're contentEditable)
if (target?.classList?.contains('code-editor')) {
return true;
}
// Block shortcuts in form elements
if (target?.tagName?.match(/INPUT|TEXTAREA|SELECT/i) || target?.isContentEditable) {
return false;
}
return true;
};
keyboardService.addFilter('conditional', conditionalFilter);Remove filtering:
// Remove a specific named filter
keyboardService.removeFilter('forms');
// Or remove all
keyboardService.clearFilters();Example: Modal Context Filtering
export class ModalComponent {
constructor() {
// When modal opens, only allow modal-specific shortcuts
this.keyboardService.addFilter('modal-scope', (event) => {
const target = event.target as HTMLElement;
// Only process events within the modal
return target?.closest('.modal') !== null;
});
}
onClose() {
// Restore normal filtering when modal closes
this.keyboardService.removeFilter('modal-scope');
}
}Performance tips
- Filters are evaluated once per keydown before scanning shortcuts. If any global filter returns false, ngx-keys exits early and clears pending sequences.
- Group-level filters are precomputed once per event; shortcuts in blocked groups are skipped without key matching.
- Keep filters cheap and synchronous. Prefer reading event.target properties (tagName, isContentEditable, classList) over layout-triggering queries.
- Use named filters to toggle contexts (modals, editors) without allocating new closures per interaction.
- Avoid complex DOM traversals inside filters; if needed, memoize simple queries or use attributes (e.g., data-no-shortcuts).
Building
To build the library:
ng build ngx-keysTesting
To run tests:
ng test ngx-keysLicense
0BSD © ngx-keys Contributors
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
