Skip to main content
AccessiGuard

How to Fix Keyboard Accessibility: A Developer's Complete Guide

27% of websites fail keyboard accessibility. Learn how to find and fix keyboard traps, missing focus indicators, and navigation issues — with code examples.

·7 min read·AccessiGuard Team
WCAG 2.1Keyboard AccessibilityWeb DevelopmentADA ComplianceCode Examples

Put your mouse away. Seriously — unplug it, hide it in a drawer, and try using your website with just your keyboard.

If you can't reach every button, link, and form field, or if you get stuck somewhere you can't escape, you've just discovered what millions of people experience every day. According to BarrierBreak, 27.1% of home pages fail to provide full keyboard accessibility. And unlike low-contrast text or missing alt attributes, automated tools catch less than half of keyboard issues.

This is one of the most impactful accessibility problems to fix — and one of the most overlooked.

Why Keyboard Accessibility Matters

Not everyone uses a mouse. People with motor disabilities, repetitive strain injuries, temporary injuries, or visual impairments rely on keyboards, switch devices, or voice controls that all map to keyboard input. Power users Tab through forms. Screen reader users navigate entirely via keyboard.

WCAG is explicit about this. Four success criteria directly address keyboard accessibility:

  • 2.1.1 Keyboard (Level A): All functionality must be available via keyboard
  • 2.1.2 No Keyboard Trap (Level A): Users must be able to navigate away from any component
  • 2.4.7 Focus Visible (Level AA): Keyboard focus indicator must be visible
  • 2.4.11 Focus Appearance (Level AA, WCAG 2.2): Focus indicators must meet minimum size and contrast

These are Level A and AA — the baseline for legal compliance. Not optional extras.

Issue #1: Click Handlers on Non-Interactive Elements

This is the most common keyboard failure I see. A developer attaches a click handler to a <div> or <span> and calls it a day. Mouse users never notice. Keyboard users can't reach it at all.

Broken:

<div class="card" onclick="openDetails()">
  View Details
</div>

This <div> isn't in the tab order. It has no keyboard event handler. It has no ARIA role. A keyboard user will Tab right past it.

Fixed — the right way:

<button class="card" onclick="openDetails()">
  View Details
</button>

Use a <button> or <a> element. Native HTML gives you keyboard focus, Enter/Space activation, and screen reader semantics for free.

Fixed — when you can't change the element:

<div class="card"
  role="button"
  tabindex="0"
  onclick="openDetails()"
  onkeydown="if(event.key === 'Enter' || event.key === ' ') openDetails()">
  View Details
</div>

You need all four: role, tabindex, click handler, AND keyboard handler. Miss any one of these and it's still broken for some users. This is why native elements are almost always the better choice.

Issue #2: Keyboard Traps

A keyboard trap is when a user Tabs into a component and can't Tab out. Modal dialogs and embedded widgets (video players, maps, third-party chat widgets) are the usual culprits.

WebAIM's 2025 analysis of the top million sites found an average of 51 errors per page — and keyboard traps are among the most severe because they completely block the user. No workaround. Dead end.

Common trap — a modal without escape handling:

<div class="modal" tabindex="-1">
  <input type="text" />
  <button>Submit</button>
  <!-- Tab from here goes... where? -->
</div>

Fixed — proper focus trapping with escape:

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

  // Move focus into the modal
  first.focus();

  modal.addEventListener('keydown', (e) => {
    // Escape closes the modal
    if (e.key === 'Escape') {
      closeModal(modal);
      return;
    }

    // Tab wraps within the modal
    if (e.key === 'Tab') {
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault();
        last.focus();
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
  });
}

The key principles: focus moves into the modal on open, Tab cycles within it, Escape closes it, and focus returns to the trigger element on close.

Even better — use the native <dialog> element, which handles most of this automatically in modern browsers:

<dialog id="myDialog">
  <form method="dialog">
    <p>Are you sure?</p>
    <button value="cancel">Cancel</button>
    <button value="confirm">Confirm</button>
  </form>
</dialog>

Issue #3: Invisible or Removed Focus Indicators

This one drives me up the wall. Somewhere around 2015, a CSS trend emerged: outline: none on everything. Designers didn't like the "ugly" blue ring. So they removed the one thing that tells keyboard users where they are on the page.

The crime:

*:focus {
  outline: none;
}

This single line makes your entire site unusable for keyboard users. It's the visual equivalent of hiding all the buttons for mouse users.

The fix — custom focus styles:

/* Remove the default only if you replace it */
:focus-visible {
  outline: 3px solid #1a73e8;
  outline-offset: 2px;
  border-radius: 2px;
}

Use :focus-visible instead of :focus. It shows the focus ring for keyboard users but hides it for mouse clicks — the best of both worlds. Every modern browser supports it.

WCAG 2.2's new Success Criterion 2.4.11 (Focus Appearance) at Level AA specifies that focus indicators must have a minimum area of a 2px solid outline around the entire component, with 3:1 contrast against the unfocused state. Plan for this now — it's the direction compliance is heading.

Issue #4: Skip Navigation Links

If your site has a header with 20 navigation links, a keyboard user has to Tab through all 20 to reach the main content. Every. Single. Page.

Add a skip link:

<body>
  <a href="#main-content" class="skip-link">
    Skip to main content
  </a>
  <header><!-- nav items --></header>
  <main id="main-content">
    <!-- page content -->
  </main>
</body>
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  padding: 8px 16px;
  background: #000;
  color: #fff;
  z-index: 100;
  transition: top 0.2s;
}

.skip-link:focus {
  top: 0;
}

Hidden by default. Appears when focused. First Tab press on the page reveals it. This is one of the simplest accessibility wins — takes five minutes and makes a huge difference.

Issue #5: Custom Dropdown Menus and Widgets

Every custom select dropdown, accordion, tab panel, or carousel needs keyboard interaction patterns. The WAI-ARIA Authoring Practices define these patterns — arrow keys for menus, Enter/Space for toggling, Home/End for jumping to first/last item.

A custom dropdown that actually works:

<div role="listbox" aria-label="Choose a color" tabindex="0"
     aria-activedescendant="option-blue">
  <div role="option" id="option-red">Red</div>
  <div role="option" id="option-blue" aria-selected="true">Blue</div>
  <div role="option" id="option-green">Green</div>
</div>
listbox.addEventListener('keydown', (e) => {
  const options = [...listbox.querySelectorAll('[role="option"]')];
  const current = options.findIndex(
    o => o.id === listbox.getAttribute('aria-activedescendant')
  );

  let next = current;
  if (e.key === 'ArrowDown') next = Math.min(current + 1, options.length - 1);
  if (e.key === 'ArrowUp') next = Math.max(current - 1, 0);
  if (e.key === 'Home') next = 0;
  if (e.key === 'End') next = options.length - 1;

  if (next !== current) {
    e.preventDefault();
    options[current].removeAttribute('aria-selected');
    options[next].setAttribute('aria-selected', 'true');
    listbox.setAttribute('aria-activedescendant', options[next].id);
  }
});

If this looks like a lot of code for a dropdown — it is. That's the cost of going custom. Use native <select> elements when you can.

How to Test

Automated tools like AccessiGuard catch many issues — missing tabindex, empty buttons, absent ARIA roles. But keyboard accessibility needs manual testing too. Here's the checklist:

  1. Unplug your mouse. Navigate your entire site using only Tab, Shift+Tab, Enter, Space, and Arrow keys
  2. Can you reach everything? Every link, button, form field, and interactive widget
  3. Can you see where you are? Focus indicator visible at all times
  4. Can you escape everything? Modals, dropdowns, embedded content — Escape or Tab should always get you out
  5. Is the tab order logical? Does it follow the visual layout, or does it jump around?
  6. Do custom widgets respond to expected keys? Arrow keys for menus, Enter for buttons, Space for checkboxes

Run this test once. You'll find problems. Everyone does.

Stop Guessing. Start Scanning.

Manual keyboard testing is irreplaceable, but you shouldn't be manually hunting for the detectable issues. AccessiGuard scans your site continuously and flags keyboard accessibility failures — missing focus indicators, empty interactive elements, improper ARIA usage — before your users hit them.

Set up monitoring in 2 minutes. Fix the issues that automated tools can catch. Then spend your manual testing time on the things only a human can find.

Try AccessiGuard free →