# Keyboard users cannot reach your div buttons or see where they are: two WCAG rows

Mega Analyzer flags clickable divs with no tabindex, and outline: none with no :focus-visible replacement. What each row reads, who it locks out, and the fix.

Author: J.A. Watte
Published: September 21, 2026
Source: https://jwatte.com/blog/blog-mega-analyzer-keyboard-and-focus/

---

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](/blog/blog-tool-ada-litigation-risk/) 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.

```html
<!-- 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.

```css
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.

```html
<div class="card" role="button" tabindex="0" data-room="12">Deluxe King</div>
```

```js
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](/tools/wcag-fix-generator/) recommends for 2.4.7: the ring shows for keyboard users and not on mouse clicks.

```css
/* 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.

```css
: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](/tools/mega-analyzer/) and confirm the two rows are gone from the Fails list. Run the [WCAG Accessibility Audit](/tools/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](/tools/wcag-fix-generator/) for a per-criterion remediation prompt, and cross-check the keyboard results against the [form accessibility post](/blog/blog-tool-form-accessibility/) 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: unset` resets every property except `unicode-bidi`, `direction` and custom properties to its inherited or initial value, which takes `outline` with 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 others `tabindex="-1"`.

## Related reading

- [Why WCAG A11y Audit Exists](/blog/blog-tool-wcag-accessibility-audit/)
- [WCAG Fix Generator: Turn the Audit's Failure List Into an AI Remediation Prompt](/blog/blog-tool-wcag-fix-generator/)
- [Most forms fail accessibility before anyone ever fills them out](/blog/blog-tool-form-accessibility/)
- [Main and nav landmarks, one title, unique IDs: the skeleton screen readers rely on](/blog/blog-mega-analyzer-landmarks-title-unique-ids/)

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.*


---

Canonical HTML: https://jwatte.com/blog/blog-mega-analyzer-keyboard-and-focus/
RSS: https://jwatte.com/feed.xml
JSON Feed: https://jwatte.com/feed.json
Hero image: https://jwatte.com/images/blog-mega-analyzer-keyboard-and-focus.webp
