Skip to main content
ADA Enforcement Alert: Over 4,000 websites faced accessibility claims last year. Check your compliance in 30 seconds →
Accessibility Shield
FeaturesFree ScannerROI CalculatorPricingFAQBlogContact
Back to all articles
Blog/WCAG 2.2 Focus Visible: A Developer's Step-by-Step Guide
Engineering
WCAG Guidelines

WCAG 2.2 Focus Visible: A Developer's Step-by-Step Guide

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.

Updated October 4, 2026
7 min read

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.


The focus criteria at a glance

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.


Why this one matters

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.


Step-by-step fix

1. Find where focus disappears

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/

2. Stop removing outlines globally

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.

3. Use :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.

4. Don't let sticky elements cover focus (2.4.11)

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.

5. Fix custom components

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.

6. Add a regression test

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.

7. Re-check on real pages

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.


Common mistakes

  • Relying on color change alone. A link that only shifts from blue to a slightly darker blue on focus usually fails 1.4.11.
  • Outline hidden by overflow: hidden. Use outline-offset with a negative value, or an inset box-shadow, inside clipped containers.
  • Focus moved into a modal but not returned. When a dialog closes, send focus back to the button that opened it.
  • Expecting an overlay to fix it. A script can draw rings on top, but your components still need to be focusable and operable. See why overlay widgets attract ADA lawsuits.

Where Accessibility Shield fits

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.


Frequently asked questions

What is the difference between :focus and :focus-visible?

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

What changed about focus in WCAG 2.2?

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.

Is outline: none always a failure?

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.

Can automated tools test focus visibility?

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.

Tags:
#WCAG 2.2
#Focus Visible
#focus-visible CSS
#Keyboard Accessibility
#Playwright
#Axe-Core

Related Articles

Mar 8, 2026

Automated vs. Manual Accessibility Testing: What Every Team Needs to Know

Understand the capabilities and boundaries of automated Axe-Core scanners vs manual assistive audits, and learn how to implement an efficient hybrid compliance strategy.

Read article
Mar 5, 2026

The Essential WCAG 2.1 AA Compliance Checklist for Web Developers

A comprehensive developer checklist covering contrast ratios, keyboard accessibility, semantic landmarks, and ARIA attributes to achieve WCAG 2.1 Level AA conformance.

Read article
Oct 4, 2026

Is My Website ADA Compliant? How to Check in 15 Minutes

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.

Read article
Accessibility Shield

Continuous automated WCAG 2.1 AA audits, hosted accessibility statements, and on-site barrier reporting for modern web teams.

Footer Navigation

Product

  • Automated Audits
  • Pricing & Plans
  • Free Audit Scanner
  • Lawsuit ROI Calculator
  • Compliance Guides

Trust & Security

  • About Our Mission
  • Security Architecture
  • Accessibility Statement
  • Contact Support

Legal & Policies

  • Privacy Policy
  • Cookie Policy
  • Terms of Service
  • 14-Day Refund Guarantee
© 2026 Accessibility Shield Inc. All rights reserved.·Zero-Tracking Platform · No cookies sold or tracked
Theme