How to meet WCAG focus requirements: 2.4.7 Focus Visible, the new 2.4.11 Focus Not Obscured, and 1.4.11 contrast. CSS you can copy and a Playwright test to keep it fixed.
To meet WCAG's focus requirements, every focusable element needs a clearly visible indicator when it receives keyboard focus (2.4.7 Focus Visible, Level AA). That indicator must not be completely hidden by sticky headers, banners or other content (2.4.11 Focus Not Obscured (Minimum), new in WCAG 2.2, Level AA). And it should contrast at least 3:1 against its surroundings (1.4.11 Non-text Contrast). In CSS, the cleanest way to do this is a strong :focus-visible outline that you never remove.
| Criterion | Level | Version | What it requires |
|---|---|---|---|
| 2.4.7 Focus Visible | AA | 2.0 | A visible focus indicator for keyboard users |
| 1.4.11 Non-text Contrast | AA | 2.1 | UI component states, including focus, at 3:1 contrast |
| 2.4.11 Focus Not Obscured (Minimum) | AA | 2.2 | The focused element isn't entirely hidden by other content |
| 2.4.12 Focus Not Obscured (Enhanced) | AAA | 2.2 | No part of the focused element is hidden |
| 2.4.13 Focus Appearance | AAA | 2.2 | Minimum size and contrast for the indicator itself |
For a typical business site targeting Level AA, the first three are the ones that count.
Keyboard users, switch users and many screen magnifier users rely on the focus ring to know where they are. Remove it and the page becomes a guessing game. It's also one of the most common regressions: a designer asks to "get rid of the blue box," someone adds outline: none, and the problem ships to every page.
Automated scanners like axe-core can't reliably detect a missing focus indicator, because it depends on how styles change between states. That's why it needs its own test, which we'll add in step 6.
Press Tab from the top of the page and watch. Note every element where you lose track of focus: links in the header, custom buttons, cards, form fields, the cookie banner. Then search your CSS for the usual causes:
grep -rnE "outline:\s*(none|0)" src/
If your reset contains this, it's the root cause:
/* Don't do this */
*:focus {
outline: none;
}
Delete it. If the design team objects to focus rings on mouse click, :focus-visible (next step) solves that without hurting keyboard users.
:focus-visible with a strong indicator:focus-visible matches when the browser decides the user needs a visible indicator, mostly keyboard navigation, and not on a mouse click. It's supported in all current browsers, so you no longer need a polyfill.
:root {
--focus-color: #1a56db;
}
:focus-visible {
outline: 3px solid var(--focus-color);
outline-offset: 2px;
}
/* On dark backgrounds, switch to a light ring */
.on-dark :focus-visible {
--focus-color: #fbbf24;
}
A 2–3 px solid outline with an offset reads well on most designs. Check it against both the element and the background behind it. 1.4.11 needs 3:1 against adjacent colors.
Sticky headers, chat buttons and cookie banners can hide the focused element as the page scrolls. scroll-padding keeps the browser from scrolling focused elements underneath a fixed header:
html {
scroll-padding-top: 5rem; /* height of your sticky header */
scroll-padding-bottom: 4rem; /* height of a sticky bottom bar, if any */
}
Also make sure non-modal pop-ups (cookie banners, promo bars) can be dismissed with the keyboard, and don't cover the main content while someone is tabbing through it.
Custom controls built from <div> and <span> usually can't receive focus at all. Use native elements first:
<!-- Not focusable, no keyboard support -->
<div class="btn" onclick="addToCart()">Add to cart</div>
<!-- Focusable, works with Enter and Space, announced as a button -->
<button type="button" class="btn" onclick="addToCart()">Add to cart</button>
If a component must be custom, give it tabindex="0", the right role, keyboard handlers, and make sure your :focus-visible styles apply to it.
Run axe-core for the automatable rules, then add a simple test that tabs through the page and checks each focused element has a visible outline or box-shadow:
// tests/a11y.spec.js
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test("home page has no automatically detectable WCAG A/AA issues", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"])
.analyze();
expect(results.violations).toEqual([]);
});
test("every focusable element shows a focus indicator", async ({ page }) => {
await page.goto("/");
for (let i = 0; i < 40; i++) {
await page.keyboard.press("Tab");
const info = await page.evaluate(() => {
const el = document.activeElement;
if (!el || el === document.body) return null;
const s = getComputedStyle(el);
const hasOutline = s.outlineStyle !== "none" && parseFloat(s.outlineWidth) > 0;
const hasShadow = s.boxShadow !== "none";
return { tag: el.tagName, text: el.textContent?.trim().slice(0, 40), visible: hasOutline || hasShadow };
});
if (!info) break;
expect(info.visible, `No focus indicator on <${info.tag}> "${info.text}"`).toBe(true);
}
});
This won't judge contrast for you, but it catches the most common failure, which is an indicator removed entirely.
Tab through the key flows (navigation, search, product page, cart, checkout, contact form) after each release. Third-party widgets and theme updates are the usual source of new problems.
overflow: hidden. Use outline-offset with a negative value, or an inset box-shadow, inside clipped containers.Accessibility Shield runs axe-core weekly against your live pages and gives each issue a code-level fix, including a prompt for AI coding tools like Cursor and Claude Code. Focus visibility still needs the keyboard check above, which is why we recommend pairing automated scans with a quick manual pass. Start with a free scan, or read the full WCAG 2.1 AA checklist.
:focus matches any focused element, including after a mouse click. :focus-visible matches only when the browser decides a visible indicator is needed, mainly during keyboard navigation. Use :focus-visible for your rings so mouse users don't see them on click and keyboard users always do.
WCAG 2.2 added 2.4.11 Focus Not Obscured (Minimum) at Level AA, plus 2.4.12 and 2.4.13 at Level AAA. 2.4.7 Focus Visible is unchanged.
Only if nothing replaces it. Removing the default outline is fine when you provide another clearly visible indicator, such as a custom outline or box-shadow with enough contrast.
Not reliably. Rule engines like axe-core can't tell whether an indicator is visible. Use a keyboard check or a custom Playwright test like the one above.
Understand the capabilities and boundaries of automated Axe-Core scanners vs manual assistive audits, and learn how to implement an efficient hybrid compliance strategy.
A comprehensive developer checklist covering contrast ratios, keyboard accessibility, semantic landmarks, and ARIA attributes to achieve WCAG 2.1 Level AA conformance.
Your site is ADA compliant only if it meets the technical requirements of WCAG 2.1 AA and you can show a good‑faith effort to keep it that way.