/ Accessibility Fixes / Keyboard Nav

How to Fix Keyboard Navigation

Keyboard navigation is the foundation of web accessibility. Screen readers, switch devices, voice control, and many motor-disability tools all rely on keyboard input. If you can't reach an element by tabbing or activate it with Enter/Space, the site is broken for a large group of users. WCAG 2.1.1 (Keyboard) is a Level A requirement — the minimum legal bar. This guide covers the full keyboard fix: tab order, focus traps, custom widgets, skip links.

1. Audit by tabbing

Step 1
Put the mouse away
Open your site. Use Tab/Shift+Tab/Enter/Space/Arrows only. Try to perform every common task: navigate menus, complete a purchase, submit a form, dismiss a notification.
Step 2
Note every failure
Watch for: elements you can't reach by Tab, controls that don't respond to Enter/Space, focus jumping unpredictably, focus disappearing after an action, modals that won't close with Escape, dropdowns that won't open with keyboard.

2. Make every interactive element focusable

Native HTML elements: focusable by default

<a>, <button>, <input>, <select>, <textarea>, <details>, <summary> all tab-focusable automatically.

Custom interactive elements: need tabindex and key handlers

<!-- WRONG -->
<div onclick="openMenu()">Menu</div>

<!-- RIGHT -->
<button onclick="openMenu()">Menu</button>

<!-- IF YOU MUST use a div -->
<div role="button" 
     tabindex="0" 
     onclick="openMenu()"
     onkeydown="if(event.key === 'Enter' || event.key === ' ') openMenu()">
  Menu
</div>

The native button approach handles all this for free. Default to native elements.

3. Fix tab order

⚠️ Tab order should follow the visual reading order. If your DOM order matches your visual layout, default behaviour works. Problems start when CSS reorders things (flex order, grid placement, absolute positioning) without DOM changes.

Avoid positive tabindex values

<!-- WRONG — puts these at the START of tab order before everything else -->
<input tabindex="1">
<input tabindex="2">
<input tabindex="3">

<!-- RIGHT — natural DOM order -->
<input>
<input>
<input>

Use tabindex values correctly

4. Eliminate focus traps

A focus trap is when Tab moves focus to an element and you can't get out. Two kinds:

Bug: unwanted trap

A widget grabs focus and doesn't release it. Often: video players that capture all key events, infinite-loop modal dialogs.

Required: modal trap

Modals SHOULD trap focus while open — Tab cycles within the modal, Escape closes. The fix is correct trap behaviour, not removal.

// Correct modal focus management
function openModal(modal) {
  const previouslyFocused = document.activeElement;
  
  // Move focus into modal
  modal.querySelector('h2').focus();
  
  // Trap Tab within modal
  modal.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') closeModal(modal, previouslyFocused);
    if (e.key === 'Tab') {
      const focusables = modal.querySelectorAll('a, button, input, [tabindex="0"]');
      const first = focusables[0];
      const last = focusables[focusables.length - 1];
      if (e.shiftKey && document.activeElement === first) {
        last.focus(); e.preventDefault();
      } else if (!e.shiftKey && document.activeElement === last) {
        first.focus(); e.preventDefault();
      }
    }
  });
}

function closeModal(modal, previouslyFocused) {
  modal.setAttribute('hidden', '');
  previouslyFocused.focus(); // restore focus to opener
}
💡 The HTML <dialog> element with showModal() handles focus trap automatically. Use it when browser support allows. Saves implementing the trap logic yourself.

5. Add a skip link

Keyboard users hitting Tab on every page first traverse the main navigation before reaching content. A skip link lets them jump directly to <main>.

<body>
  <a href="#main" class="skip-link">Skip to main content</a>
  <header>...nav...</header>
  <main id="main" tabindex="-1">...content...</main>
</body>

<style>
.skip-link {
  position: absolute;
  top: -100px;
  left: 8px;
  background: #1e3a8a;
  color: white;
  padding: 12px 16px;
  z-index: 1000;
  text-decoration: none;
}
.skip-link:focus {
  top: 8px;
}
</style>

The skip link is visually hidden until focused via Tab. First-time keyboard users see it appear; mouse users never see it.

6. Custom widget keyboard patterns

Each widget pattern has standard keyboard expectations. Following ARIA Authoring Practices means users coming from desktop applications find familiar behaviour.

Dropdown menu

Tabs

Combobox / autocomplete

7. Test thoroughly

Step 1
Manual keyboard test
Tab through every page completing every workflow. Mouse-free.
Step 2
Automated tests with Playwright
test('can navigate menu with keyboard', async ({ page }) => {
  await page.goto('/');
  await page.keyboard.press('Tab'); // skip link
  await page.keyboard.press('Tab'); // logo
  await page.keyboard.press('Tab'); // first nav link
  await page.keyboard.press('Enter');
  await expect(page).toHaveURL(/about/);
});

♿ Re-run the Accessibility audit

Verify keyboard-navigation findings are cleared.

Run Accessibility Audit →
Related Guides: Accessibility Fixes  ·  Fix Focus Indicators  ·  Fix Link Text  ·  Accessibility Guide
💬 Got a problem?