javascript
47 lines · 9 steps
Building a keyboard shortcut manager in JavaScript
A class that maps normalized key combos to callbacks and dispatches them on keydown.
Explained by
highlit
1class ShortcutManager {
2 constructor(target = window) {
3 this.target = target;
4 this.bindings = new Map();
5 this.handleKeyDown = this.handleKeyDown.bind(this);
6 this.target.addEventListener('keydown', this.handleKeyDown);
7 }
8
9 normalize(combo) {
10 const parts = combo.toLowerCase().split('+').map((p) => p.trim());
11 const modifiers = { ctrl: false, alt: false, shift: false, meta: false };
12 let key = '';
13 for (const part of parts) {
14 if (part === 'cmd' || part === 'meta') modifiers.meta = true;
15 else if (part === 'ctrl' || part === 'control') modifiers.ctrl = true;
16 else if (part === 'alt' || part === 'option') modifiers.alt = true;
17 else if (part === 'shift') modifiers.shift = true;
18 else key = part;
19 }
20 return `${+modifiers.ctrl}${+modifiers.alt}${+modifiers.shift}${+modifiers.meta}:${key}`;
21 }
22
23 bind(combo, callback) {
24 const id = this.normalize(combo);
25 if (!this.bindings.has(id)) this.bindings.set(id, new Set());
26 this.bindings.get(id).add(callback);
27 return () => this.unbind(combo, callback);
28 }
29
30 unbind(combo, callback) {
31 const handlers = this.bindings.get(this.normalize(combo));
32 if (handlers) handlers.delete(callback);
33 }
34
35 handleKeyDown(event) {
36 const id = `${+event.ctrlKey}${+event.altKey}${+event.shiftKey}${+event.metaKey}:${event.key.toLowerCase()}`;
37 const handlers = this.bindings.get(id);
38 if (!handlers || handlers.size === 0) return;
39 event.preventDefault();
40 for (const handler of handlers) handler(event);
41 }
42
43 destroy() {
44 this.target.removeEventListener('keydown', this.handleKeyDown);
45 this.bindings.clear();
46 }
47}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Normalizing input into a canonical key lets registration and lookup share one comparable format.
- 2Storing callbacks in a Set per combo allows multiple handlers and easy removal without duplicates.
- 3Returning an unbind closure from bind gives callers a clean, self-contained way to detach.
Related explainers
php
<?php class NameParser {
Parsing a full name into components in PHP
string-parsing
arrays
normalization
Intermediate
8 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
javascript
import { useEffect, useRef, useState } from 'react'; export function useDelayedFlag(active, delay = 300) { const [visible, setVisible] = useState(false);
Delaying a loading spinner with a React hook
custom-hooks
debouncing
cleanup
Intermediate
8 steps
javascript
const SWIPE_THRESHOLD = 80; const MAX_TRANSLATE = 120; export function attachSwipeToDismiss(element, onDismiss) {
Building a swipe-to-dismiss gesture in JS
touch-events
gesture-detection
dom-manipulation
Intermediate
10 steps
python
from typing import Any, Sequence, Mapping def render_markdown_table(
Rendering an aligned Markdown table in Python
string formatting
data transformation
closures
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-keyboard-shortcut-manager-in-javascript-explained-javascript-ff15/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.