Two rows on the Mega Analyzer's A11y (WCAG) tab describe the same person: someone moving through your page with the Tab key instead of a mouse. N custom control(s) unreachable by keyboard (WCAG 2.1.1) means a div or span is doing a button's job without being in the tab order, so that person can never land on it. Focus ring removed (outline: none) with no :focus-visible replacement (WCAG 2.4.7) means that even on the controls they can reach, your CSS has erased the only cue that shows where they are. One row is about reaching the control; the other is about seeing that you did.
What the check actually tests
Both rows come from the shared quick-check module at /js/wcag-quick-checks.js, which the Mega Analyzer and the Site Analyzer both load. The module runs against the server-rendered HTML the analyzer fetched, parsed into a document. Your JavaScript never executes, which shapes what each rule can and cannot see.
Keyboard reachability (rule 18 in the module) queries nine selectors: div, span, p, li and img elements with an onclick attribute, plus div and span elements carrying role="button" or role="link". It then throws out anything hidden (inside noscript, template or a [hidden] ancestor, an inline display: none or visibility: hidden, or aria-hidden="true"), anything that already has a tabindex attribute, anything sitting inside an a[href], a button or an element with a tabindex, and anything that contains a real control (a[href], button, input, select, textarea or a [tabindex] element). What survives is a clickable thing with no keyboard path in or around it. The fail row reads N custom control(s) unreachable by keyboard (WCAG 2.1.1) with the detail "A div or span with an onclick or role="button" is not in the tab order and never receives Enter or Space. Use a real <button> or <a href>, or add tabindex="0" plus a keydown handler." There is no pass row. When the count is zero the row simply does not appear, and the Critical fails tile at the top of the card does not count it. The cross-audit pills under a fail are labeled Keyboard reachability.
Three things do not trip this rule, and the first is the big one. A click handler attached with addEventListener, which is how a framework's onClick prop ends up wired, leaves no onclick attribute in the HTML, so a React or Vue clickable div passes unless the template also renders role="button". Second, the rule asks only whether a tabindex attribute exists, so tabindex="-1" passes even though it takes the element out of the Tab sequence. Third, the tag list is fixed: a section, td or svg with an onclick, or an <a> with no href, is not queried. The reverse also holds. A div with role="button" whose tabindex a script adds after load fails the row every time, because the analyzer only ever sees the HTML the server sent.
Focus ring removed (rule 19) reads CSS text, not elements. The analyzer assembles that text from every inline <style> block plus the first two same-origin stylesheets linked from the page (up to 300,000 characters each), strips comments, and looks for two things. A "killer" is any rule whose selector contains :focus but not :focus-visible and whose declaration block sets outline: none or outline: 0, with no box-shadow (other than none) and no border or border-color in the same block. A "restore" is any rule whose selector contains :focus-visible and whose block sets outline or box-shadow to something other than none or 0. The row fails only when at least one killer exists and no restore exists anywhere in the scanned CSS. The fail wording is Focus ring removed (outline: none) with no :focus-visible replacement (WCAG 2.4.7), and the detail reads "Rule", then the first offending selector (its first 60 characters), then "{ outline: none } hides the only indicator a keyboard user has of where they are. Restore it with a :focus-visible rule (outline: 2px solid; outline-offset: 2px)." Like rule 18, it is silent when clean.
The exclusions matter here too. :focus { outline: none; box-shadow: 0 0 0 3px blue } passes, because the block swaps in a replacement. :focus:not(:focus-visible) { outline: none } passes, because the selector contains :focus-visible. A single .btn:focus-visible { outline: 2px solid } anywhere in the scanned CSS restores the whole page in the rule's eyes, even if the page has forty other focusable selectors it does not cover, which makes this a page-level judgment rather than a per-selector one. And the regex wants none or a bare 0 followed by whitespace, a semicolon or the end of the block, so outline: 0px, outline-width: 0 and outline-style: none all slip past. So does any outline: none in a third-party stylesheet, a stylesheet on a CDN subdomain (a different origin), an @import, or the third and later same-origin sheets. A clean row is evidence, not proof.
Why it matters
Screen reader users navigate by keyboard because a pointer has nothing to read. People with tremors, repetitive strain injuries or limited hand mobility use a keyboard or a switch device that emulates one. And a share of everyone else tabs through forms because it is faster. WCAG 2.1.1 Keyboard, a Level A criterion, says all functionality of the content is operable through a keyboard interface; Level A is the minimum conformance level, the floor.
A <div onclick> fails that floor in three separate ways. A div is not focusable, so Tab skips it as if it were a paragraph. It has no activation behavior, so even if you give it focus, Enter and Space do nothing unless you write the handler yourself. And it exposes no role, so a screen reader announces the text with no hint that it does anything. A native <button> gives you all three for free, plus a name from its content and a disabled state. The ARIA Authoring Practices Guide's button pattern spells out the contract you have to reproduce by hand if you insist on a div: when the button has focus, Enter activates it and Space activates it.
The focus ring is the other half. WCAG 2.4.7 Focus Visible, Level AA, says any keyboard operable interface has a mode of operation where the keyboard focus indicator is visible. For a sighted keyboard user that indicator is the whole interface; without it, pressing Tab is like moving a mouse with the cursor turned off. For years the reset stylesheets everyone copied set outline: 0 on :focus, because designers did not want the ring showing after a mouse click, and themes built on those resets still carry the line. The :focus-visible pseudo-class exists to settle exactly that complaint. Per MDN, it matches when the browser's heuristics decide focus should be made evident, which in practice means keyboard navigation shows the ring and a mouse click on a button does not. It has shipped in every current engine since Safari 15.4 in 2022, so the original reason for the reset is gone.
Two neighboring criteria in WCAG 2.2 raise the bar the analyzer cannot measure. 1.4.11 Non-text Contrast (Level AA) requires 3:1 contrast for the visual information that identifies a component's state, and the Understanding document applies that to focus indicators, so a pale gray ring on white can satisfy 2.4.7 and fail 1.4.11. 2.4.11 Focus Not Obscured (Minimum), new at Level AA in 2.2, says a focused component must not be entirely hidden by author-created content, which is the sticky header and the cookie banner covering the link you just tabbed to. 2.4.13 Focus Appearance, at Level AAA, goes further and specifies the indicator's minimum area (a 2 CSS pixel perimeter) and 3:1 change between focused and unfocused states.
There is a practical reason these two rows carry weight out of proportion to the fix. A keyboard walk-through is the cheapest manual test in accessibility: Tab through the page, press Enter on each control, watch for the ring. Nobody needs a scanner to produce the sentence "the menu could not be operated with a keyboard," which is why it reads so plainly in a demand letter. The ADA litigation risk post covers who sends those and to whom.
How to fix it
1. Change the tag. For anything that performs an action on the page, a <button type="button"> is the fix, and the onclick moves over unchanged. For anything that navigates, it is a link and wants an <a href>. This is the part most sites get wrong: they keep the div and bolt ARIA onto it, when swapping the tag removes the need for every attribute and handler in step 2.
<!-- Before: invisible to Tab, deaf to Enter and Space, announced as plain text -->
<div class="btn" onclick="openBooking()">Book now</div>
<!-- After: focusable, activatable and announced as a button, same handler -->
<button type="button" class="btn" onclick="openBooking()">Book now</button>
<!-- If it goes somewhere, it is a link -->
<a class="btn" href="/rooms/deluxe-king/">Deluxe King</a>
The reason people reach for a div is that a button carries browser styling. Reset the pieces you need rather than everything. all: unset on a button resets outline along with everything else, so it removes the browser's default focus ring too, and because it never writes the string outline: none, the analyzer cannot see it.
button.btn {
font: inherit;
color: inherit;
background: none;
border: 0;
padding: 0;
cursor: pointer;
}
2. If the tag truly cannot change, reproduce the button contract by hand and put the static parts in the HTML, not in a script. The analyzer reads the served markup, so tabindex="0" added by JavaScript after load still fails the row.
<div class="card" role="button" tabindex="0" data-room="12">Deluxe King</div>
document.querySelectorAll('[role="button"][data-room]').forEach(el => {
const activate = () => openRoom(el.dataset.room);
el.addEventListener('click', activate);
el.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault(); // Space would otherwise scroll the page
activate();
}
});
});
tabindex="0" puts the element into the sequential focus order at its position in the DOM. The preventDefault on Space matters; without it, browsers treat Space as page-down and scroll when the user tries to activate the control. A role="link" div gets the same treatment, but ask first why it is not an <a href>.
3. Find the reset. Search your CSS for outline: none, outline: 0, outline-width: 0 and outline-style: none. The analyzer only catches the first two forms, and only in the sheets it fetched, so search all of them, including the theme's and the framework's. Then either delete the line or replace the whole block with the pair below, which is the shape the WCAG Fix Generator recommends for 2.4.7: the ring shows for keyboard users and not on mouse clicks.
/* Delete this wherever a reset or theme put it */
a:focus, button:focus, .btn:focus { outline: none; }
/* Keep the ring for keyboard focus, drop it after a pointer click */
:focus-visible {
outline: 2px solid #1d4ed8;
outline-offset: 2px;
}
:focus:not(:focus-visible) {
outline: none;
}
Pick the ring color against your backgrounds, not your brand palette. It needs 3:1 against whatever sits next to it, and a site with a dark header and a light body has two "next to it" colors. A two-tone ring, an inner white outline with an outer colored box-shadow, handles both without a second rule: whichever color contrasts with the local background carries the 3:1.
:focus-visible {
outline: 2px solid #ffffff;
outline-offset: 0;
box-shadow: 0 0 0 4px #1d4ed8;
}
4. Verify with a keyboard first, then the tools. Click in the address bar, press Tab, and keep pressing. Every control you can click should take focus in a sensible order, show the ring, and respond to Enter (and Space for buttons). Watch for the ring vanishing under the sticky header, which is 2.4.11 and something no static scanner can see. Then re-run the Mega Analyzer and confirm the two rows are gone from the Fails list. Run the WCAG Accessibility Audit for the deep pass. Its 2.1.1 rule casts a different net: any onclick on a tag other than a, button, input, select or textarea that has neither a role nor a tabindex, so it catches the section and td cases the quick check skips but ignores a role="button" div that has no onclick. Its 2.4.7 rule counts every outline: none or outline: 0 in the served HTML (linked stylesheets are invisible to it) and reports the count as a critical failure when :focus-visible appears nowhere in that HTML, or as a warning when it does. Paste the failure list into the WCAG Fix Generator for a per-criterion remediation prompt, and cross-check the keyboard results against the form accessibility post if the controls in question live inside a form.
When to leave it alone
A card with an onclick that wraps a real link or button is fine, and the rule already skips it. The inner control is the keyboard path; the outer click target is a convenience for mouse users. Keep the link, do not add a second tabindex to the wrapper.
tabindex="-1" on a role="button" element is often correct. The APG's roving tabindex pattern for composite widgets (a toolbar, a tab list, a menu) keeps exactly one item at tabindex="0" and the rest at -1, and moves focus among them with the arrow keys. Adding tabindex="0" to every item would break the pattern. The row passes these because the attribute is present; know why before you "fix" it.
:focus { outline: none; box-shadow: 0 0 0 3px ... } in one block is a legitimate custom ring, and the row treats it as one. Showing that ring on mouse click as well as keyboard focus is a design choice, not a WCAG failure; :focus-visible is the refinement the analyzer suggests, not something the criterion requires.
The keyboard criterion itself has an exception for functions that depend on the path of the user's movement and not just the endpoints, such as freehand drawing. That exception belongs to the criterion, not to this row, since a <canvas> is not on the rule's tag list. A signature pad with a div-based "clear" button next to it still needs the button fixed.
Finally, a fail from a selector that matches nothing on the page (dead CSS in a theme) is a real rule that will apply to the next element that matches. Remove it anyway, but do not treat it as an emergency.
Fact-check notes and sources
- Source: https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html establishes WCAG 2.1.1 Keyboard (Level A): all functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes, with the exception for input that depends on the path of the user's movement and not just the endpoints.
- Source: https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html establishes WCAG 2.4.7 Focus Visible (Level AA): any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible.
- Source: https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html establishes WCAG 2.4.11 Focus Not Obscured (Minimum) (Level AA, new in WCAG 2.2): when a component receives keyboard focus it is not entirely hidden due to author-created content.
- Source: https://www.w3.org/WAI/WCAG22/Understanding/focus-appearance.html establishes WCAG 2.4.13 Focus Appearance (Level AAA): the focus indicator area is at least as large as a 2 CSS pixel thick perimeter of the unfocused component and has a 3:1 contrast ratio between focused and unfocused states.
- Source: https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html establishes WCAG 1.4.11 Non-text Contrast (Level AA): visual information required to identify user interface components and states has a 3:1 contrast ratio against adjacent colors, which the Understanding text applies to focus indicators.
- Source: https://www.w3.org/TR/WCAG22/#cc1 establishes the conformance levels and that Level A is the minimum level of conformance.
- Source: https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible establishes that the pseudo-class applies when the browser determines via heuristics that focus should be made evident, which lets a page show the ring for keyboard focus and not after a mouse click, and carries the browser compatibility table showing support across current engines, with Safari 15.4 (March 2022) the last to add it.
- Source: https://developer.mozilla.org/en-US/docs/Web/CSS/all establishes that
all: unsetresets every property exceptunicode-bidi,directionand custom properties to its inherited or initial value, which takesoutlinewith it. - Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/tabindex establishes that
tabindex="0"places an element in the sequential keyboard focus order at its DOM position and that a negative value makes it focusable by script only. - Source: https://www.w3.org/WAI/ARIA/apg/patterns/button/ establishes the button pattern's keyboard interaction: when the button has focus, Enter activates it and Space activates it.
- Source: https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/ establishes the roving tabindex technique for composite widgets, where one element has
tabindex="0"and the otherstabindex="-1".
Related reading
- Why WCAG A11y Audit Exists
- WCAG Fix Generator: Turn the Audit's Failure List Into an AI Remediation Prompt
- Most forms fail accessibility before anyone ever fills them out
- Main and nav landmarks, one title, unique IDs: the skeleton screen readers rely on
If you fix sites for other people, the keyboard walk-through is also the best sales demo you own: two minutes of Tab on the client's own homepage, with the ring gone and the menu unreachable, explains the invoice better than any report. The $20 Dollar Agency is built around deliverables that demonstrate themselves like that.
This post is informational, not legal advice. Mentions of third parties are nominative fair use. No affiliation is implied.