# How to Handle Keyboard Events for Keyboard-Accessible Web Apps

> Learn to handle keyboard events for accessible web apps. Filter repeats, whitelist keys, and map inputs to actions for full keyboard navigation and assistive technology support.

- Repository: [Jisan Mia/dom-projects](https://github.com/jisan-mia/dom-projects)
- Tags: how-to-guide
- Published: 2026-03-04

---

**Handle keyboard events by attaching global `keydown` listeners, filtering auto-repeat with `e.repeat`, validating against a whitelist of supported keys, and mapping inputs to semantic actions—ensuring full keyboard accessibility for users with motor impairments or screen readers.**

Building keyboard-accessible web applications requires more than just adding `tabindex` attributes. In the **dom-projects** repository by jisan-mia, several projects demonstrate production-ready patterns for handling keyboard events that support both utility applications and interactive games. These implementations show how to capture keyboard input reliably while preventing unwanted side effects and maintaining clean separation between input logic and business logic.

## Setting Up Global Keyboard Listeners

The foundation of keyboard accessibility starts with capturing input regardless of which element currently has focus. According to the dom-projects source code, global listeners enable shortcuts and controls that work everywhere in the application.

In [`projects/retro-calculator/js/script.js`](https://github.com/jisan-mia/dom-projects/blob/main/projects/retro-calculator/js/script.js), the calculator attaches a document-level listener to ensure users can operate the interface without clicking buttons first:

```javascript
document.addEventListener("keydown", (e) => {
  // handler logic
});

```

For game-style interactions, [`projects/flappy-bird/flappybird.js`](https://github.com/jisan-mia/dom-projects/blob/main/projects/flappy-bird/flappybird.js) uses the same pattern but attaches the listener only after initialization:

```javascript
function startGame() {
  document.addEventListener("keydown", moveBird);
}

```

This approach ensures that **keyboard events** are captured immediately, supporting users who cannot or prefer not to use a mouse.

## Filtering and Validating Keyboard Input

Raw keyboard input requires sanitization to prevent duplicate actions and unexpected behavior. The dom-projects repository implements a three-layer validation strategy.

**Filter auto-repeat events.** When a user holds down a key, browsers fire repeated `keydown` events. The calculator in [`projects/retro-calculator/js/script.js`](https://github.com/jisan-mia/dom-projects/blob/main/projects/retro-calculator/js/script.js) guards against this:

```javascript
if (e.repeat) return;

```

**Normalize key values.** Use the standardized `e.key` property rather than legacy `keyCode` or `which` properties. This provides consistent identifiers like `"Enter"`, `"Backspace"`, and `"c"` across browsers and locales:

```javascript
const { key } = e;

```

**Validate against a whitelist.** Define an explicit array of supported keys to limit the handler to meaningful input:

```javascript
const supportedKeyboardKeys = [
  "0","1","2","3","4","5","6","7","8","9",
  ".","+","-","*","/","=","Enter","Backspace","c","C","d","D"
];

if (!supportedKeyboardKeys.includes(key)) return;

```

This whitelist approach prevents accidental side effects when users interact with assistive technologies or press system shortcuts.

## Mapping Keys to Semantic Actions

Once validated, map specific keys to domain-specific functions rather than embedding logic directly in the event handler. This separation of concerns makes the code testable and extensible.

In [`projects/retro-calculator/js/script.js`](https://github.com/jisan-mia/dom-projects/blob/main/projects/retro-calculator/js/script.js), the handler delegates to a `calculator` class instance:

```javascript
if (key === "c" || key === "C") {
  calculator.clear();
} else if (key === "d" || key === "D" || key === "Backspace") {
  calculator.removeDigit();
} else if (key === "=" || key === "Enter") {
  calculator.calculate();
} else if (key === "*") {
  calculator.choseOperation("×");
} else if (key === "/") {
  calculator.choseOperation("÷");
} else if (key === "+" || key === "-" || key === "^") {
  calculator.choseOperation(key);
} else {
  calculator.addDigit(key);
}

```

This pattern ensures that **keyboard-accessible** functionality mirrors mouse-driven interactions exactly, providing equivalent access for users with motor impairments.

## Accessibility Best Practices in Implementation

Beyond basic event handling, the dom-projects repository demonstrates several critical accessibility patterns.

**Prevent default scrolling when necessary.** While the calculator examples do not require `e.preventDefault()` because they whitelist specific keys, applications using arrow keys for navigation should call `e.preventDefault()` to stop page scrolling.

**Provide visual focus indicators.** Ensure interactive elements display visible focus outlines. The repository achieves this implicitly by using button-like elements with proper `data-` attributes and CSS focus states.

**Graceful degradation.** When a key is not in the whitelist, the handler does nothing, allowing the browser to process the event normally. This ensures screen readers and other assistive technologies maintain their default functionality.

**Clean up listeners.** Although the demo pages are static, single-page applications should remove listeners on component teardown to prevent memory leaks:

```javascript
document.removeEventListener("keydown", handler);

```

## Complete Implementation Examples

### Calculator with Whitelist Validation

The retro calculator demonstrates command-style keyboard handling where specific keys trigger specific functions:

```javascript
// projects/retro-calculator/js/script.js
const supportedKeyboardKeys = [
  "0","1","2","3","4","5","6","7","8","9",
  ".","+","-","*","/","=","Enter","Backspace","c","C","d","D"
];

document.addEventListener("keydown", (e) => {
  if (e.repeat) return;
  const { key } = e;

  if (!supportedKeyboardKeys.includes(key)) return;

  if (key === "c" || key === "C") {
    calculator.clear();
  } else if (key === "d" || key === "D" || key === "Backspace") {
    calculator.removeDigit();
  } else if (key === "=" || key === "Enter") {
    calculator.calculate();
  } else if (key === "*") {
    calculator.choseOperation("×");
  } else if (key === "/") {
    calculator.choseOperation("÷");
  } else if (key === "+" || key === "-" || key === "^") {
    calculator.choseOperation(key);
  } else {
    calculator.addDigit(key);
  }
});

```

### Game-Style Instant Actions

The Flappy Bird implementation shows action-style handling where any key triggers the same behavior:

```javascript
// projects/flappy-bird/flappybird.js
function startGame() {
  document.addEventListener("keydown", moveBird);
}

function moveBird() {
  // Flap/wing animation logic
}

```

This simplicity illustrates that global listeners can serve both complex command mappings and simple trigger responses.

### Reusable Keyboard Handler Utility

For applications requiring multiple keyboard-controlled components, extract the pattern into a helper:

```javascript
// projects/retro-calculator/js/utils.js
export function attachKeyboard(handler, whitelist = [], { preventRepeat = true } = {}) {
  const listener = (e) => {
    if (preventRepeat && e.repeat) return;
    if (whitelist.length && !whitelist.includes(e.key)) return;
    handler(e);
  };
  document.addEventListener("keydown", listener);
  return () => document.removeEventListener("keydown", listener);
}

```

Usage:

```javascript
import { attachKeyboard } from "./utils.js";

const detach = attachKeyboard((e) => {
  // Handle specific key logic
}, supportedKeyboardKeys);

// Cleanup when component unmounts
detach();

```

## Summary

- **Attach global listeners** using `document.addEventListener("keydown", handler)` to capture input regardless of focus state.
- **Filter auto-repeat** by checking `e.repeat` to prevent duplicate actions when keys are held down.
- **Validate input** against a whitelist array of supported keys to limit handler scope and prevent side effects.
- **Map to semantic actions** by delegating to specific methods like `calculator.clear()` or `moveBird()` rather than embedding logic in handlers.
- **Clean up listeners** using `removeEventListener` when components teardown to avoid memory leaks in single-page applications.

## Frequently Asked Questions

### How do I prevent a keyboard event from repeating when a user holds down a key?

Check the `repeat` property on the event object. In [`projects/retro-calculator/js/script.js`](https://github.com/jisan-mia/dom-projects/blob/main/projects/retro-calculator/js/script.js), the implementation uses `if (e.repeat) return;` at the start of the handler to ignore auto-repeat events fired by the browser when a key is held continuously. This prevents duplicate calculations or rapid-fire game actions.

### What is the best way to handle different keys performing different actions in a web app?

Use a whitelist array combined with conditional logic or a lookup object. The calculator example in [`projects/retro-calculator/js/script.js`](https://github.com/jisan-mia/dom-projects/blob/main/projects/retro-calculator/js/script.js) defines a `supportedKeyboardKeys` array and uses a cascade of `if/else` statements to map specific keys to specific calculator methods like `calculator.choseOperation()` or `calculator.clear()`. This keeps the event handler clean and the business logic testable.

### Should I use `keydown` or `keyup` for keyboard accessibility?

Use `keydown` for immediate feedback and `keyup` when you need to know when a key is released. The dom-projects repository uses `keydown` exclusively for both the calculator and Flappy Bird demos because it provides the lowest latency for user actions. For accessibility purposes, `keydown` is generally preferred for triggering actions, while `keyup` is useful for stopping continuous actions like scrolling or movement.

### How do I make sure my keyboard handlers don't interfere with screen readers?

Validate inputs against a strict whitelist and avoid calling `e.preventDefault()` unless absolutely necessary. By checking `if (!supportedKeyboardKeys.includes(key)) return;` as shown in [`projects/retro-calculator/js/script.js`](https://github.com/jisan-mia/dom-projects/blob/main/projects/retro-calculator/js/script.js), you allow unhandled keys to pass through to the browser and assistive technologies. Only prevent default behavior for keys that would otherwise scroll the page or conflict with your specific shortcuts.