← Back to Accessalyze

By Genesis AI Services · April 21, 2026 · 8 min read · Keyboard Accessibility

How to Fix Keyboard Navigation Issues on Your Website

Quick test: Open your site, put your mouse aside, and use only Tab, Shift+Tab, Enter, Space, and arrow keys to navigate. If you get lost, get trapped, or can't reach something — that's a WCAG violation.

Keyboard accessibility (WCAG 2.1 — principle 2: Operable) is critical for users who cannot use a mouse: people with motor disabilities, power users, and anyone using a keyboard-driven interface or switch device. Here are the most common failures and how to fix them.

Issue 1 — Hidden Focus Indicator (outline: none)

The problem: Many CSS resets and design systems include :focus { outline: none } to remove the browser's default blue ring. This makes it impossible to tell where keyboard focus is.
/* BAD — removes focus visibility entirely */
* { outline: none; }
:focus { outline: none; }
The fix: Use :focus-visible to show a custom focus ring only for keyboard navigation (not on mouse click).
/* Remove outline on mouse click, but show it for keyboard */
:focus { outline: none; }
:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 3px;
  border-radius: 4px;
}

Issue 2 — Keyboard Trap (Focus Gets Stuck)

The problem: A modal, dropdown, or widget captures focus and the user cannot Tab out of it. Or the user presses Escape but the modal does not close.

Note: modals should trap focus within themselves while open — that is correct behavior. The failure is when focus cannot be escaped at all.

See how 321 websites scored →

View the 2026 Report
The fix: Implement proper focus trapping with an Escape key handler and a close button.
// Trap focus inside modal
function trapFocus(modal) {
  const focusable = modal.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  modal.addEventListener('keydown', (e) => {
    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();
      }
    }
    if (e.key === 'Escape') closeModal();
  });
}

function openModal(modal) {
  modal.removeAttribute('hidden');
  trapFocus(modal);
  modal.querySelector('[autofocus], button, [tabindex]').focus();
}

function closeModal() {
  modal.setAttribute('hidden', '');
  triggerButton.focus(); // return focus to trigger
}

Issue 3 — Non-Keyboard-Accessible Custom Controls

The problem: A custom dropdown, date picker, or tab panel is built from <div> elements with click handlers, but has no keyboard support.
The fix: Always prefer native HTML elements. When you must build custom widgets, implement the correct ARIA pattern and keyboard interactions.
<!-- BAD: non-keyboard-accessible -->
<div class="tab" onclick="selectTab(1)">Overview</div>

<!-- GOOD: full keyboard and ARIA support -->
<div role="tablist" aria-label="Product tabs">
  <button role="tab"
          aria-selected="true"
          aria-controls="panel-overview"
          id="tab-overview">Overview</button>
  <button role="tab"
          aria-selected="false"
          aria-controls="panel-features"
          id="tab-features"
          tabindex="-1">Features</button>
</div>

<div role="tabpanel" id="panel-overview" aria-labelledby="tab-overview">
  ...content...
</div>
// Arrow key navigation for tab widgets
tablist.addEventListener('keydown', (e) => {
  const tabs = [...tablist.querySelectorAll('[role="tab"]')];
  const idx = tabs.indexOf(document.activeElement);
  if (e.key === 'ArrowRight') {
    tabs[(idx + 1) % tabs.length].focus();
  } else if (e.key === 'ArrowLeft') {
    tabs[(idx - 1 + tabs.length) % tabs.length].focus();
  }
});

Issue 4 — Wrong Tab Order (tabindex Misuse)

The problem: Using positive tabindex values (e.g. tabindex="3") creates a global tab sequence that overrides document order and is confusing to keyboard users.
/* BAD — positive tabindex values break natural order */
<button tabindex="3">First visually but third in DOM</button>
<button tabindex="1">Third visually but first in tab order</button>
The fix: Use only tabindex="0" (adds to natural order) and tabindex="-1" (removes from tab order, focusable via JS). Fix the DOM order to match visual order.
<!-- Good: DOM order matches visual order, no positive tabindex -->
<button>First</button>
<button>Second</button>
<button>Third</button>

<!-- OK: programmatically focusable but not in tab order -->
<div tabindex="-1" id="error-message">Error: invalid email</div>

Issue 5 — Skip Navigation Link Missing

Without a skip link, keyboard users must Tab through every navigation item on every page before reaching main content.

<!-- First element in <body> -->
<a href="#main-content" class="skip-link">Skip to main content</a>

<!-- Later in the page -->
<main id="main-content">...</main>
/* Visually hidden until focused */
.skip-link {
  position: absolute;
  top: -100px;
  left: 0;
  background: #2563eb;
  color: #fff;
  padding: 8px 16px;
  z-index: 1000;
  font-weight: 700;
  transition: top 0.1s;
}
.skip-link:focus {
  top: 0;
}

Find Keyboard Accessibility Failures on Your Site

Accessalyze detects missing skip links, focus issues, and interactive elements that fail keyboard navigation automatically.

Run a Free Accessibility Scan →

← Back to Accessalyze

Accessalyze - Free WCAG 2.1 scanner that writes the fix code for you | Product Hunt

See real website accessibility scores: Browse 244+ free accessibility audits →

Try it yourself

Enter your website URL to get a free accessibility score.

Check your website accessibility score free Scan Now →