A visitor presses Tab on your home page and nothing seems to happen. Or something happens, but they cannot see it: focus has moved somewhere, yet no outline shows where. They try to open the Services dropdown and the submenu never appears. A newsletter popup opens and Tab walks through the page hidden behind it, or worse, focus gets stuck inside an embedded widget and the only way out is to close the tab.
These are keyboard and focus failures. They hit people who cannot use a mouse: people with motor disabilities, screen reader users, and people using switch devices or voice control. They are common audit findings, and most come from a few design decisions you can find and reverse.
What is happening
At any moment, one element on a page has keyboard focus. Pressing Tab moves focus to the next focusable element in the order the elements appear in the HTML source, not the order they appear on screen. Natively focusable elements are links with an href, buttons, form fields, and a few others. Anything else, such as a div with a click handler, is skipped unless the developer added tabindex="0" and keyboard handling.
Browsers draw a focus indicator (usually an outline) on the focused element. Many themes and CSS resets remove it with outline: none because designers dislike the outline appearing after mouse clicks. Once it is gone, a keyboard user is navigating blind.
Six WCAG 2.2 success criteria cover this ground:
| Criterion |
Level |
What it requires in practice |
| 2.1.1 Keyboard |
A |
Every function (menus, sliders, tabs, forms, popups) works with a keyboard alone |
| 2.1.2 No Keyboard Trap |
A |
If focus can move into a component, it can move out again with the keyboard, and any unusual exit method is explained |
| 2.4.1 Bypass Blocks |
A |
A way to skip repeated blocks such as the header and navigation, usually a skip link, landmarks and headings |
| 2.4.3 Focus Order |
A |
Focus moves in an order that preserves meaning and operability |
| 2.4.7 Focus Visible |
AA |
The keyboard focus indicator is visible |
| 2.4.11 Focus Not Obscured (Minimum) |
AA |
New in WCAG 2.2: a focused element is not entirely hidden by content the author created, such as a sticky header or cookie banner |
One clarification saves a lot of confusion. Keeping focus inside an open modal dialog is correct behavior, not a keyboard trap, as long as the dialog can be closed from the keyboard (with Escape or a close button) and focus is released when it closes. A trap is when there is no keyboard way out.
Before you start
You need a desktop browser with developer tools (Chrome, Edge or Firefox), access to your theme or page builder settings, and a place to add custom CSS: Appearance, Customize, Additional CSS on classic themes; the Styles panel in the Site Editor on block themes; or your page builder global CSS or class settings. Test on a staging copy if you have one, and take a backup before editing theme files.
How to run the commands in this guide. Browser console commands run in the developer tools Console tab (press F12, or Command+Option+I on a Mac) while your site is open. The one server command runs over SSH in your site folder, the one that contains wp-config.php. On macOS, Safari does not move focus to links with Tab by default: turn on “Press Tab to highlight each item on a webpage” in Safari Settings, Advanced, or test in Chrome or Firefox instead.
Step 1: Test the whole page with only a keyboard
Click in the address bar, then press Tab repeatedly. Use these keys:
- Tab and Shift+Tab: move forward and back between interactive elements.
- Enter: follow a link or press a button.
- Space: press a button, check a checkbox, scroll the page when nothing is focused.
- Arrow keys: move within radio groups, select boxes, sliders and tab sets.
- Escape: close a menu, popup or dialog.
Write down every place where you lose track of focus, cannot reach something you could click with a mouse, cannot open or close something, or where focus jumps somewhere surprising. To see exactly where focus is when the outline is invisible, paste this into the Console:
document.addEventListener("focusin", function (event) { console.log(event.target); });
There are no placeholders. The code listens for every focus change and logs the focused element. Now press Tab on the page and watch the Console: each line is the element that just received focus, and hovering a line highlights that element on the page. Good output is a sensible sequence of links, buttons and fields you can see. Bad output includes elements you cannot see on screen (hidden submenu links, offscreen slides, a closed mobile menu) and long runs of the same hidden component. In Chrome you can also click the eye icon (“Create live expression”) above the Console and enter document.activeElement to show the focused element continuously.
Step 2: Make focus visible again (2.4.7)
Confirm the cause
Tab to a link whose focus you cannot see, right-click it, and choose Inspect. In the Styles pane, open the :hov panel and check :focus-visible (and :focus). Look for a rule setting outline: none, outline: 0 or outline-width: 0; crossed-out rules have been overridden. The file name beside the rule tells you which theme, plugin or builder stylesheet removed the outline. To search the whole theme on the server:
grep -rnE "outline: ?(none|0)" wp-content/themes/THEME_FOLDER --include=*.css
Replace THEME_FOLDER with your active theme folder name, which you can see in Appearance, Themes (hover the theme for its details) or with wp theme list --status=active. -r searches recursively, -n prints line numbers, -E enables the pattern syntax, and --include=*.css limits the search to stylesheets. Good output is nothing at all, or matches that also add another visible indicator. Bad output is a line like style.css:412:a:focus { outline: none; } with no replacement style. If the theme is clean, search wp-content/plugins too.
The fix
Do not edit the theme file directly (an update will overwrite it). Add a global rule in your custom CSS area instead:
:focus-visible {
outline: 3px solid #1A1A1A;
outline-offset: 2px;
}
.site-header :focus-visible,
.dark-section :focus-visible {
outline-color: #FFFFFF;
}
The colors are examples. Replace #1A1A1A with a color that contrasts at least 3:1 against your page background, and replace .site-header and .dark-section with the CSS classes your theme or builder uses for dark areas (inspect them to find out). :focus-visible matches only when the browser judges a visible indicator useful, which in practice means keyboard focus, so mouse users do not see outlines after every click. outline-offset moves the ring away from the element so it does not merge with borders. Good result: every link, button and field shows a clear ring when you Tab to it. Bad result: the ring shows on some elements but not others, which means a more specific rule elsewhere still removes it; inspect that element and match its selector.
The focus ring is itself a visual element, so WCAG 1.4.11 Non-text Contrast expects it to reach 3:1 against the colors next to it. If your site mixes light and dark sections, a two-tone ring (a white outline with a dark box-shadow around it) stays visible on both.
In page builders, check the global styles or class editor for a focus state before writing CSS, and test with Tab: a hover style is not a focus style.
Step 3: Fix menus you cannot open or use with Tab (2.1.1)
Confirm the cause
Tab to a top-level menu item that has a submenu. Watch the focus log from Step 1. You will see one of three patterns:
- Focus skips the submenu entirely. The submenu opens only on mouse hover and is hidden with
display: none the rest of the time.
- Focus disappears for several presses. The submenu links are hidden with
opacity: 0 or moved offscreen but are still focusable, so focus is moving through invisible links.
- The mobile menu toggle cannot be reached. The hamburger icon is a
div or span, not a button, so it is not focusable at all.
The quick fix: open on focus within
If submenus open on hover in CSS, a one-line addition makes them open when focus is inside the item as well:
.main-menu li:hover > .sub-menu,
.main-menu li:focus-within > .sub-menu {
display: block;
}
Replace .main-menu and .sub-menu with your menu selectors (WordPress classic menus commonly use sub-menu for nested lists; inspect yours). :focus-within matches an element when it or any descendant has focus. Good result: Tab onto the parent item opens the submenu, Tab moves through its links, and the submenu closes when focus leaves. Bad result: the submenu opens but its links are still skipped, which means a script or another rule controls visibility; move to the robust fix.
The robust fix: a disclosure button
The pattern recommended by the W3C ARIA Authoring Practices Guide for site navigation is a disclosure: a real button next to (or instead of) the parent link that toggles the submenu and announces its state. Note that the ARIA menu and menubar roles are meant for application menus, like a desktop program menu, and add keyboard expectations most website menus do not meet. A plain list of links with disclosure buttons is simpler and better supported.
<nav aria-label="Main">
<ul>
<li>
<a href="/services/">Services</a>
<button type="button" aria-expanded="false" aria-controls="SUBMENU_ID">
<span class="screen-reader-text">Services submenu</span>
</button>
<ul id="SUBMENU_ID" hidden> ... </ul>
</li>
</ul>
</nav>
SUBMENU_ID is any unique ID you choose for each submenu, such as services-submenu. aria-expanded tells screen readers whether the submenu is open; a script flips it between false and true and removes or adds hidden on the list. screen-reader-text is the visually hidden text class most WordPress themes provide; it gives the icon-only button a name. Good result: Tab reaches the button, Enter or Space opens the submenu, a screen reader announces “Services submenu, button, collapsed” then “expanded”, and Escape closes it and returns focus to the button.
WordPress and builder settings
On block themes, select the Navigation block, and in its settings panel turn on the submenu option to open on click rather than hover. Click-to-open submenus are generated as buttons with aria-expanded. On classic themes, menu behavior comes from the theme JavaScript, so if the theme fails, the cleanest fix is often a theme update or a different menu. In page builders, open the menu or nav element and look for options that control how dropdowns open (hover or click), whether a toggle button is shown, and how the mobile menu toggle is built. Change one option at a time and repeat the keyboard test.
Step 4: Fix popups that trap focus or leak it (2.1.2)
Popups fail in two opposite ways. A trap is when focus enters something (an embedded iframe, a chat widget, a video player) and Tab or Shift+Tab cannot leave. A leak is when a modal opens but focus stays on the page behind, so a keyboard user tabs through hidden content while the popup sits on top. Leaks are more common; traps are more severe.
Confirm the cause
Trigger each popup with the keyboard. With the focus log running, check: does focus move into the popup when it opens? Does Tab stay inside it while it is open? Does Escape close it? Does focus return to the button that opened it? If the popup opens on page load or on a timer, check that a keyboard user can reach its close button with a few Tabs.
The fix
The most reliable approach for custom code is the native HTML dialog element opened with its showModal() method. As documented on MDN, a modal dialog makes everything outside it inert (unfocusable and hidden from assistive technology), closes on Escape, and moves focus into the dialog when it opens. Add aria-labelledby pointing at the dialog heading so it is announced properly, and make sure there is a visible close button.
For a custom-built overlay that cannot use dialog, apply the inert attribute to the main page wrapper while the overlay is open and remove it on close; this is supported in all current major browsers. Move focus into the overlay on open and back to the trigger on close.
For popup and cookie banner plugins, check their settings for keyboard or accessibility options: closing on Escape, focusing the popup on open, and returning focus on close. If a plugin cannot do these things, it fails 2.1.1 and 2.1.2 no matter how it looks, and replacing it is the fix.
Step 5: Add a working skip link (2.4.1)
Confirm the cause
Load any page and press Tab once. A link reading “Skip to content” or similar should appear as the first focus stop. Press Enter: focus should move to the start of the main content, and the next Tab should land on the first link inside it, not back in the header.
The fix
Block themes get one automatically: WordPress core inserts a skip link when the template contains a main element, and points it at that element. If your block theme has no skip link, edit the template in the Site Editor and set the main content Group block to use the main HTML element (in the block Advanced settings). Classic themes usually include a skip link in header.php. Page builder headers often replace the theme header and lose it, so add one as the very first item in the builder header:
<a class="skip-link screen-reader-text" href="#MAIN_ID">Skip to content</a>
...
<main id="MAIN_ID" tabindex="-1"> ... </main>
MAIN_ID is the ID of your main content wrapper; inspect the page to find it, or set one in your builder (for example main-content). tabindex="-1" lets the target receive focus from the link without adding it to the Tab order, which makes the skip work reliably across browsers. The theme screen-reader-text class hides the link until it receives focus; check that your theme reveals it on focus, or the skip link itself will be invisible. Good result: first Tab shows the link, Enter moves you past the navigation. Bad result: the link appears but Enter does nothing, which means the ID does not match.
Step 6: Fix focus order and hidden focus (2.4.3 and 2.4.11)
Focus order
Two things scramble focus order. The first is a positive tabindex, which pulls an element ahead of everything else. Find them in the Console:
Array.from(document.querySelectorAll("[tabindex]")).filter(function (el) { return el.tabIndex > 0; });
No placeholders. It lists every element whose tabIndex is greater than zero. Good output is an empty array, shown as []. Bad output lists elements; remove their positive tabindex values (use 0 or nothing) in the plugin or builder setting that added them.
The second is CSS reordering: flex-direction: row-reverse, the order property, grid placement, and builder options such as “reverse columns on mobile”. These move things visually but not in the source, so Tab jumps around. Fix the source order in the builder instead of reversing it visually.
Focus not obscured
Sticky headers, sticky “Book now” bars and cookie banners can sit on top of the element that just received focus. Tab down a long page and watch for focus vanishing under them. The fix recommended in the W3C techniques is CSS scroll padding, which tells the browser to keep a strip clear when it scrolls focused elements into view:
html {
scroll-padding-top: HEADER_HEIGHT;
scroll-padding-bottom: BANNER_HEIGHT;
}
Replace HEADER_HEIGHT with the height of your sticky header plus a little margin (for example 96px; measure it in the Elements panel box model) and BANNER_HEIGHT with the height of any sticky bottom bar, or delete that line. Good result: focused elements always stop just below the header. Also make sure a cookie banner can be dismissed early in the Tab order, so it does not cover content for the whole visit.
Advanced: focus styles that survive forced colors and dark mode
Windows contrast themes (forced colors mode) replace author colors with the user system colors and remove box-shadow. If your design uses a box-shadow as its only focus indicator, focus becomes invisible for exactly the users who most need it. The fix is to keep a transparent outline alongside the shadow: outline: 2px solid transparent. Transparent outlines are invisible normally, but in forced colors mode the browser paints them in a system color. To test, open Chrome developer tools, press Command+Shift+P or Control+Shift+P, run “Show Rendering”, and set “Emulate CSS media feature forced-colors” to active. Then Tab through the page again.
If you want to exceed the minimum, WCAG 2.4.13 Focus Appearance (Level AAA) gives measurable targets: the indicator area should be at least as large as a 2 CSS pixel thick perimeter of the component, with at least 3:1 contrast between the focused and unfocused pixels. A solid 2 or 3 pixel outline in a strongly contrasting color meets that on most components and is a good default for any design system.
When to contact your host
Keyboard problems live in your theme, plugins and builder, not the server, so most fixes are yours or your designer. Open a ticket with your host when your CSS fix works on staging but not on the live site even after you clear your builder cache, because a server or CDN cache may still be serving the old stylesheet. Send the page URL, the stylesheet URL you expected to change, the time you published the change in UTC, and the response headers of the stylesheet request (from the Network tab), which show whether it came from cache. You can check caching yourself first with our diagnostic tools.
A note on legal questions: this guide is technical help, not legal advice. If you are fixing keyboard issues because of a complaint or demand letter, involve a lawyer for that side of it.