Skip to content
Back to skills

Apply Keyboard Accessibility

ASecurity

Use when building interactive UI components — to ensure every action achievable with a mouse is also achievable with a keyboard alone, with no traps or dead ends.

  • 4 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 8, 2026
ai-agentsjavascriptjava

Works with

  • cli

Security analysis

A100/100

Scanned September 8, 2026

npx -y skills add jeffreytse/grimoire-core --skill apply-keyboard-accessibility --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Apply Keyboard Accessibility?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Apply Keyboard Accessibility
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/jeffreytse-apply-keyboard-accessibility/badge)](https://www.skillsdirectory.com/skills/jeffreytse-apply-keyboard-accessibility)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: apply-keyboard-accessibility
description: Use when building interactive UI components — to ensure every action achievable with a mouse is also achievable with a keyboard alone, with no traps or dead ends.
source: W3C WCAG 2.1 SC 2.1.1 Keyboard (Level A), SC 2.1.2 No Keyboard Trap (Level A); WAI-ARIA Authoring Practices 1.2 (keyboard interaction patterns); MDN Web Docs keyboard accessibility guide
tags: [accessibility, wcag, a11y, keyboard-navigation, tab-order, focus, developer, interactive-components]
related: [apply-focus-management, apply-skip-navigation, design-accessibility-standards]
---

# Apply Keyboard Accessibility

Ensure every interactive element is reachable and operable by keyboard — Tab to reach it, Enter/Space to activate it, Escape to dismiss it, and arrow keys for internal navigation.

## Why This Is Best Practice

**Adopted by:** WCAG 2.1 SC 2.1.1 and 2.1.2 are Level A — the absolute baseline
required by every accessibility law worldwide (Section 508, EU EN 301 549, UK PSBAR
2018, Australian DDA). Motor disability affects 2.6% of the US population; keyboard-only
operation is essential for users who cannot use a mouse (tremors, paralysis, switch
access). Power users also rely on keyboard for speed.
**Impact:** WebAIM survey 2024 found keyboard navigation is the primary input method for
21% of screen reader users and an essential fallback for many more. Custom widgets
(datepickers, dropdowns, sliders) fail keyboard access in the majority of implementations
without deliberate effort — the WAI-ARIA Authoring Practices exist specifically to
document correct keyboard patterns.
**Why best:** Native HTML elements (`<button>`, `<input>`, `<select>`, `<a href>`) are
keyboard accessible by default. Every custom widget built on `<div>` or `<span>`
requires explicit keyboard implementation. The cost of retrofitting is 3–5× the cost
of building it correctly.

Sources: W3C WCAG 2.1 SC 2.1.1, 2.1.2 (2018); WAI-ARIA Authoring Practices 1.2;
WebAIM Screen Reader User Survey #10 (2024)

## Steps

### Step 1: Use native elements — they're keyboard accessible by default

```html
<!-- Wrong — div requires manual keyboard implementation -->
<div class="btn" onclick="submit()">Submit</div>

<!-- Right — button is keyboard accessible, focusable, and announced correctly -->
<button type="submit">Submit</button>
```

Use: `<button>` for actions, `<a href>` for navigation, `<input>` for data entry,
`<select>` for dropdowns of options, `<details>` for disclosure widgets.
Build custom widgets only when native elements cannot meet the design requirement.

### Step 2: Implement the correct keyboard pattern for each widget type

Per WAI-ARIA Authoring Practices:

| Widget | Tab behavior | Internal navigation |
|--------|-------------|---------------------|
| Button | Focusable | Enter/Space = activate |
| Link | Focusable | Enter = follow |
| Checkbox | Focusable | Space = toggle |
| Radio group | One tab stop | Arrow keys = change selection |
| Listbox/select | One tab stop | Arrow keys = navigate, Enter = select |
| Menu | One tab stop | Arrow keys = navigate, Enter/Space = select, Escape = close |
| Dialog | Trap focus inside | Escape = close, Tab cycles within |
| Slider | Focusable | Arrow keys = adjust value |
| Tabs | One tab stop per tablist | Arrow keys = change tab |

```javascript
// Example: arrow key navigation for a custom listbox
listbox.addEventListener('keydown', (e) => {
  switch (e.key) {
    case 'ArrowDown':
      e.preventDefault();
      focusNextOption();
      break;
    case 'ArrowUp':
      e.preventDefault();
      focusPrevOption();
      break;
    case 'Enter':
    case ' ':
      e.preventDefault();
      selectFocusedOption();
      break;
    case 'Escape':
      closeListbox();
      break;
  }
});
```

### Step 3: Never use `tabindex > 0`

```html
<!-- Wrong — positive tabindex creates an unpredictable tab order -->
<div tabindex="3">Third in tab order? Probably not.</div>

<!-- Right — tabindex="0" adds to natural DOM order -->
<div role="button" tabindex="0" onclick="...">Custom button</div>

<!-- Right — tabindex="-1" makes focusable programmatically, not via Tab -->
<div id="modal-content" tabindex="-1">Dialog content</div>
```

`tabindex > 0` creates a separate tab sequence that runs before the natural order —
producing a confusing jump that most users do not expect.

### Step 4: Ensure no keyboard trap exists

```javascript
// Wrong — modal that doesn't handle Tab creates a trap
// (keyboard focus leaves the modal, never to return)

// Right — confine Tab to modal contents
modal.addEventListener('keydown', (e) => {
  if (e.key !== 'Tab') return;

  const focusable = modal.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  if (e.shiftKey && document.activeElement === first) {
    e.preventDefault();
    last.focus();
  } else if (!e.shiftKey && document.activeElement === last) {
    e.preventDefault();
    first.focus();
  }
});
```

See `apply-focus-management` for the full modal focus trap pattern.

### Step 5: Test keyboard-only before every release

Manual keyboard test checklist:
- [ ] Tab visits every interactive element in logical DOM order
- [ ] Shift+Tab moves backward through the same order
- [ ] Enter/Space activates every button and link
- [ ] Escape closes modals, dropdowns, and tooltips
- [ ] Arrow keys navigate within composite widgets (menus, sliders, tabs)
- [ ] No focus is lost after a dynamic update (AJAX load, route change)

## When NOT to Use

- **Non-interactive elements** — purely presentational elements need no keyboard support. Don't add `tabindex="0"` to static text, images without actions, or decorative elements.

## Common Mistakes

**`onclick` without `onkeydown`.** A `<div>` with `onclick` responds to mouse click but not Enter or Space. Add `role="button"`, `tabindex="0"`, and keyboard event handling — or switch to `<button>`.

**Arrow keys that also scroll the page.** Arrow key handlers must call `e.preventDefault()` to prevent the page from scrolling while navigating a widget.

**Tab order that doesn't match visual order.** If the DOM order doesn't match visual layout (due to CSS flexbox/grid reordering), Tab order diverges from visual order and confuses sighted keyboard users. Fix the DOM order; use CSS only for visual reordering.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…