Fixing Keyboard Navigation
Plenty of people never touch a mouse. Some have a tremor or limited hand movement, some use a switch device or voice control that sends keystrokes, and many screen reader users move through pages with the keyboard alone.
If a menu only opens on hover, a button is really a clickable box, or a pop-up swallows the Tab key, those visitors are stuck. This guide shows how to find each keyboard failure, what the WCAG rules actually require, and the code changes that make every control reachable, usable and escapable.
- What the keyboard rules in WCAG ask for
- Why sites fail with the keyboard
- How to test a page with the keyboard
- The fix, step by step
- Code patterns that work
- Composite widgets: tabs, sliders, carousels and grids
- Where the fixes live on common platforms
- Edge cases and questions of judgement
- Keeping it fixed
- Questions and answers
What the keyboard rules in WCAG ask for
Keyboard access is not one rule but a small family of them. Most are at level A, the most basic level of WCAG 2.2, which means a failure here is treated as a barrier rather than an inconvenience. The Accessibility Checker reports the ones it can detect in code, and the rest need a quick manual test.
| Success criterion | Level | In plain English |
|---|---|---|
| 2.1.1 Keyboard | A | Everything you can do with a mouse, you can also do with keys alone. Freehand drawing is the classic exception. |
| 2.1.2 No Keyboard Trap | A | If focus can move into something, it can move out again with standard keys, or the page tells you how. |
| 2.1.4 Character Key Shortcuts | A | Single-letter shortcuts can be turned off or remapped, or only work when that control has focus. |
| 2.4.1 Bypass Blocks | A | There is a way to jump past repeated blocks such as the header and main menu. |
| 2.4.3 Focus Order | A | Focus moves in an order that keeps meaning and makes sense. |
| 3.2.1 On Focus | A | Simply landing on a control does not submit a form, open a new window or move you elsewhere. |
| 2.4.7 Focus Visible | AA | You can see where focus is. Covered in its own guide. |
| 2.4.11 Focus Not Obscured (Minimum) | AA | A sticky header, footer or banner never completely hides the focused item. |
Visible focus styles have their own guide: fixing focus indicators. This page concentrates on whether focus can get somewhere, what happens when it arrives, and whether it can leave.
Why sites fail with the keyboard
Browsers give you keyboard support for free when you use the right HTML. A <button>
is focusable and responds to Enter and Space. A link with an href is focusable and responds
to Enter. Form fields, <select>, <details> and <summary>
all work without a line of script. Keyboard failures nearly always come from replacing those elements
with something that only listens for clicks.
- Clickable boxes. A
<div>or<span>with a click listener is invisible to the Tab key and ignores Enter. - Links with no destination. An
<a>with nohrefis not focusable at all, andhref="#"used as a button jumps to the top of the page when scripts fail. - Hover-only menus. Submenus that appear on
:hovernever appear for someone tabbing through the header. - Hand-built dialogs. A pop-up that is only a positioned box lets focus wander behind it, or grabs focus and never lets go.
- Positive tabindex values. A
tabindexof 1 or more drags an element to the front of the queue and scrambles the order for everything else. - Hidden but focusable content. Off-canvas menus, collapsed panels and slides moved
off-screen with
transformoropacitystill take focus, so the outline vanishes into empty space. - Third-party widgets. Chat launchers, review carousels, booking iframes and cookie banners are often written with little keyboard testing.
How to test a page with the keyboard
An automated scan can spot a clickable <div> or a positive tabindex,
but it cannot press keys and see what happens. The manual test takes about ten minutes per template and
finds far more.
| Key | What it should do |
|---|---|
| Tab / Shift+Tab | Move forward and back through links, buttons and fields. |
| Enter | Follow a link, press a button, submit a form. |
| Space | Press a button, tick a checkbox, open a select list. Scrolls the page when nothing interactive has focus. |
| Arrow keys | Move within a group: radio buttons, tabs, menus, sliders, select options. |
| Escape | Close a dialog, menu, tooltip or autocomplete list. |
| Home / End | Jump to the first or last item in lists, tab sets and sliders that support it. |
Mac users: switch Tab on first
On a Mac, Safari does not move to links with Tab out of the box. Turn on Press Tab to highlight each item on a webpage in Safari's Advanced settings, or hold Option while pressing Tab. Firefox and Chrome on macOS follow the system-wide Keyboard navigation switch in System Settings under Keyboard. If you skip this, you will think the whole site is broken when it is not.
Spotting hidden focus
When the outline disappears, open the browser console and run document.activeElement
after each Tab. It returns the element that currently has focus, which quickly exposes a hidden mobile
menu link or an off-screen slide. In Chrome you can also add a live expression for document.activeElement
so it updates as you tab.
The fix, step by step
Step 1
Walk every key journey with the keyboard only
Pick the tasks that matter most, such as opening the main menu, searching, adding a product to the basket, booking, and sending the contact form. Put the mouse out of reach and complete each one using only Tab, Shift+Tab, Enter, Space, the arrow keys and Escape. Write down every control you could not reach, could not operate, or could not leave, together with the template it lives in, because the fix usually belongs in that template.
Step 2
Replace clickable divs with real buttons and links
Search the templates for click listeners attached to div, span,
li and img elements. If the control goes to another page or address, make it
an <a> with a real href. If it does something on the current page, such
as opening a panel or adding to the basket, make it a <button type="button">. Reset
the button styles in CSS so the design does not change, and delete any hand-written keydown handling
the native element now makes redundant.
Step 3
Clean up tabindex and the tab order
Find
every tabindex in the code. Change any value of 1 or higher to 0 or remove
it, then fix the order by moving elements in the HTML rather than forcing it. Use tabindex="-1"
only for things you focus with script, such as a heading after a route change. Check that CSS reordering
with order, flex-direction: row-reverse or grid placement has not made the
visual order disagree with the order focus follows.
Step 4
Make menus and dropdowns open from the keyboard
Give each submenu a real <button> next to or in place of the parent
label, with aria-expanded set to false or true to match its state,
and toggle the submenu when it is pressed. Keep the hover behaviour for mouse users if you like, but
never make hover the only way in. Close the open submenu when Escape is pressed, return focus to its
button, and close it when focus moves out of the menu.
Step 5
Contain focus in dialogs, then hand it back
Build modal pop-ups with the native <dialog> element opened by
showModal(), which keeps focus inside, makes the page behind it inert and closes on Escape.
If you cannot use it, add the inert attribute to the rest of the page while the pop-up is
open. Move focus into the dialog when it opens, and when it closes send focus back to the control that
opened it, so the visitor carries on from the same spot.
Step 6
Add a skip link past the header
Make
the first focusable item on every page a link reading Skip to main content that points
at #main, and put id="main" on the <main> element. Hide
it off-screen until it receives focus, then slide it into view at the top left with strong contrast.
Press Tab once on a fresh page load to confirm it appears, press Enter, then press Tab again and check
that focus lands on the first link inside the content.
Step 7
Remove traps in embeds and third-party widgets
Tab into and out of every embedded map, video player, booking iframe, chat launcher, review carousel and cookie banner. If focus goes in but cannot come out with Tab or Shift+Tab, check the supplier's settings for an accessible mode, update to the current version, or report the fault and replace the widget. As a stopgap, place a clear link before the embed that lets keyboard users skip over it to the content that follows.
Code patterns that work
A button that looks like anything
Designers often avoid <button> because of the default grey box. Strip the defaults
once and it can look like text, an icon or a card.
<button type="button" class="plain-btn" aria-expanded="false" aria-controls="filters">
Filter results
</button>
.plain-btn {
background: none;
border: 0;
padding: 0;
font: inherit;
color: inherit;
cursor: pointer;
}
A disclosure menu
<nav aria-label="Main">
<ul>
<li>
<a href="/services/">Services</a>
<button type="button" class="sub-toggle" aria-expanded="false" aria-controls="sub-services">
<span class="visually-hidden">Show Services submenu</span>
</button>
<ul id="sub-services" hidden>
<li><a href="/services/boilers/">Boiler servicing</a></li>
<li><a href="/services/repairs/">Emergency repairs</a></li>
</ul>
</li>
</ul>
</nav>
document.querySelectorAll('.sub-toggle').forEach(function (btn) {
var panel = document.getElementById(btn.getAttribute('aria-controls'));
btn.addEventListener('click', function () {
var open = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!open));
panel.hidden = open;
});
btn.parentElement.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && !panel.hidden) {
panel.hidden = true;
btn.setAttribute('aria-expanded', 'false');
btn.focus();
}
});
});
Using the hidden attribute (or display: none) matters: it removes the closed
submenu links from the tab order, whereas hiding them with opacity or a negative position
leaves them focusable. The button needs an accessible name; see fixing
ARIA labels for the options.
A modal dialog
<button type="button" id="open-quote">Get a quote</button>
<dialog id="quote" aria-labelledby="quote-title">
<h2 id="quote-title">Request a quote</h2>
<form method="dialog">
<!-- fields -->
<button value="send">Send</button>
<button value="cancel" formnovalidate>Cancel</button>
</form>
</dialog>
var opener = document.getElementById('open-quote');
var dlg = document.getElementById('quote');
opener.addEventListener('click', function () { dlg.showModal(); });
dlg.addEventListener('close', function () { opener.focus(); });
Current browsers generally restore focus to the opener themselves when a modal dialog closes, but
the explicit opener.focus() costs nothing and covers older versions and dialogs closed by
script.
A skip link
<a class="skip-link" href="#main">Skip to main content</a>
...
<main id="main" tabindex="-1">
.skip-link { position: absolute; left: -9999px; top: 0; }
.skip-link:focus { left: 1rem; top: 1rem; z-index: 1000;
background: #111; color: #fff; padding: .75rem 1rem; }
The tabindex="-1" on <main> makes older browsers move the keyboard
starting point along with the scroll position. It does not add <main> to the Tab sequence.
Composite widgets: tabs, sliders, carousels and grids
Some controls are groups rather than single buttons. The convention, set out in the W3C ARIA Authoring Practices Guide, is that Tab moves into the group once and out again, and the arrow keys move inside it. That keeps a set of twelve tabs or a date picker from costing twenty Tab presses.
- Tabs. Only the selected tab has
tabindex="0"; the others have-1. Left and Right move between tabs, and Tab goes straight to the open panel. This technique is called a roving tabindex. - Radio groups. Native radio buttons with the same
namealready behave this way. Do not rebuild them. - Sliders and range inputs. A native
<input type="range">responds to the arrow keys. A custom slider needs the same, plus Page Up, Page Down, Home and End if it covers a wide range. WCAG 2.2 also asks that dragging is never the only way to set a value. - Carousels. Provide visible previous, next and pause buttons that are real buttons. Slides not on screen should be hidden from focus, and auto-rotation should stop when anything inside the carousel has focus.
- Scrollable areas. A box with its own scroll bar, such as a wide price table, cannot
be scrolled with the keyboard unless it or something inside it can take focus. Recent Chrome versions
make such areas focusable automatically, but other browsers may not, so add
tabindex="0",role="region"and a shortaria-labelto the scrolling wrapper.
<details> and <summary> make an accordion with no script; <select>
is a fully keyboard-accessible dropdown; <dialog> handles modal focus.Where the fixes live on common platforms
The code changes above are the same everywhere, but where you make them differs.
- WordPress. Menu and header markup comes from the theme, and pop-ups, sliders and forms come from plugins. Block themes get a skip link added automatically by WordPress core. See accessibility in WordPress for how to trace a fault to the theme or a plugin.
- Shopify. The header, mega menu, cart drawer and quick-view pop-ups live in theme sections and snippets written in Liquid, while review, upsell and chat widgets are injected by apps. Accessibility in Shopify covers editing those safely.
- React and Next.js. Keyboard problems usually come from click handlers on divs in components and from focus not being moved after client-side navigation. Accessibility in React and Next.js covers lint rules and focus management.
- Static and hand-built sites. Fix the shared header and footer include once, and every page benefits.
Edge cases and questions of judgement
Text editors that use Tab
A rich text editor or code box may reasonably use Tab to indent. That is allowed under 2.1.2 as long as there is a way out that the page explains, for example a note saying that Ctrl+M switches the Tab key back to moving focus, or that Escape leaves the editor.
Focus on page load
Do not autofocus a search box or newsletter field on every page. It skips screen reader users past the page heading and can make mobile keyboards pop up unexpectedly. Autofocus is reasonable on a page whose only purpose is that field, such as a dedicated search page or a login screen.
Things that happen on focus
A select list that navigates to a new page as soon as the value changes is a trap for keyboard users, because pressing the down arrow to read the options fires the change. Add a Go button instead. The same applies to fields that submit the form when they receive focus.
Single-key shortcuts
If your site or web app uses single letters as shortcuts, such as J and K to move between items, speech recognition users can trigger them by accident when dictating. Offer a setting to turn them off or require a modifier key such as Ctrl.
Sticky headers and cookie banners
When a focused link slides under a fixed header or a cookie banner, WCAG 2.2 criterion 2.4.11 is failed.
Adding scroll-padding-top to the html element equal to the header height usually
solves the header case; banners should be dismissible and should not cover focused content while open.
Keeping it fixed
- Add a lint rule to the build that flags click listeners on non-interactive elements and positive
tabindexvalues. - Put a keyboard-only walk-through of the main journeys into your release checklist, especially after a theme update or a new plugin or app.
- Ask suppliers of widgets and apps whether their product has been tested with a keyboard and a screen reader before you install it.
- Run the Accessibility Audit on each template after changes and compare with the previous report.
- Keep a small library of known-good patterns, such as the button reset, disclosure menu, dialog and skip link above, and reuse them instead of rebuilding.
1. How do I know if my website works with a keyboard?
Put the mouse away, click in the browser address bar and press Tab repeatedly. You should see focus move through every link, button and field in a sensible order, and you should be able to operate each one with Enter, Space or the arrow keys. Try the main menu, search, forms, pop-ups and the basket. If anything cannot be reached, cannot be used, or holds on to focus so you cannot leave, that is a keyboard failure to fix.
2. What is a keyboard trap?
A keyboard trap is any part of a page that focus can enter but cannot leave using standard keys such as Tab, Shift+Tab or Escape. Common examples are embedded booking forms in iframes, poorly built pop-ups and some chat widgets. WCAG 2.1.2 No Keyboard Trap is a level A criterion. A modal dialog that keeps focus inside on purpose is not a trap, provided Escape or a visible close button lets the visitor out.
3. Is tabindex="0" bad for accessibility?
No. A tabindex of zero adds an element to the normal tab order in its natural position, which is correct for a custom control that genuinely needs focus, such as a scrollable table wrapper. The problem value is anything of 1 or higher, which forces the element ahead of everything else and scrambles the order. A tabindex of minus one is also useful: it lets script focus an element without adding it to the Tab sequence.
4. Why can I not tab to links in Safari?
Because Safari on a Mac only moves between form fields with Tab by default. Links and buttons are skipped unless you turn on the setting called Press Tab to highlight each item on a webpage, found in the Advanced section of Safari settings, or hold Option while pressing Tab. This is a browser preference, not a fault on your site, so switch it on before testing, then judge the site as you would in any other browser.
5. Do I need a skip link if my site has good headings?
Yes, it is still worth adding. Headings and landmarks help screen reader users jump around, but people who use a keyboard without a screen reader cannot use those shortcuts, and they may face dozens of Tab presses through the header and menu on every page. A skip link to the main content is the simplest way to meet WCAG 2.4.1 Bypass Blocks for them, and it costs a few lines of HTML and CSS.
6. Should dropdown menus open on hover or on click?
Either can work for mouse users, but keyboard users need a control they can press. The most robust pattern is a real button beside each parent link that opens and closes its submenu and reports its state with aria-expanded, with Escape closing it again. You can keep hover opening for mouse users as well. Relying on the CSS focus-within selector alone is fragile, because the submenu links must still be reachable in a sensible order.
7. Does fixing keyboard access help screen reader users too?
Very often, yes. Screen readers on desktop computers are driven from the keyboard, and they rely on the same native buttons, links and form controls that make keyboard access work. Replacing a clickable div with a button fixes the Tab key and also gives the screen reader a role and a name to announce. Screen readers do have extra reading shortcuts, so test with one as well, but keyboard fixes are the foundation for both groups.
The Accessibility Audit tests your page against WCAG 2.2 level AA and lists each finding with the element that failed and the success criterion behind it, so you can see which controls need fixing first.
Open Accessibility Fixes โ