Focus Ring Colors That Pass WCAG 2.2
SC 2.4.7, 2.4.11, and 2.4.13 define different jobs. Token focus rings for all three.
The accessibility audit passed. The focus ring looked fine in the Figma handoff. Then a keyboard user tabbed through the marketing page and lost the focused link under a sticky cookie banner, could not see a 1px blue halo on a navy button, and filed a bug that referenced three different success criteria numbers the engineering team had never read. Design had approved a ring color. Engineering had implemented it. Neither had tested tab order with fixed chrome present, and neither had measured change-of-contrast between focused and unfocused pixels.
Focus styling is not one requirement. WCAG 2.2 splits it across visibility, obscuring, and appearance—each with different thresholds and test methods. Getting the success criterion numbers wrong in documentation or procurement language ships the wrong fix: a team optimizes outline color while the real failure is scroll-padding under a fixed footer. Conflating the criteria is how organizations ship blue rings that pass design review and fail keyboard users in production.
This reference maps SC 2.4.7 Focus Visible at Level AA, SC 2.4.11 Focus Not Obscured Minimum at Level AA, and SC 2.4.13 Focus Appearance at Level AAA to token patterns you can implement in CSS today. Most contracts stop at AA, meaning 2.4.7 and 2.4.11. 2.4.13 is AAA unless your statement of work explicitly requires it. Many teams target 2.4.13 as an internal quality bar anyway because it encodes what keyboard users actually need: thickness, area, and change-of-contrast between states. The related criterion 2.4.12 Focus Not Obscured Enhanced at Level AAA demands the focused component not be hidden at all, not merely partially visible. This article covers 2.4.11 minimum partial visibility, not 2.4.12.
Three criteria, three different jobs
The three success criteria answer different questions. SC 2.4.7 at Level AA asks whether there is a visible focus indicator in keyboard mode. SC 2.4.11 at Level AA asks whether the focused component is at least partly visible and not entirely hidden by author-created content. SC 2.4.13 at Level AAA asks whether the indicator is large enough and contrasting enough between focused and unfocused states. They stack. Passing one does not pass the others. A ring can exist and still leave the focused button fully under a cookie banner. A ring can sit in view and still be too thin to perceive. A ring can be thick and still fail change-of-contrast because the unfocused state was already a similar gray.
| Criterion | Level | Question it asks |
|---|---|---|
| 2.4.7 Focus Visible | AA | Is there a visible focus indicator in keyboard mode? |
| 2.4.11 Focus Not Obscured Minimum | AA | Is the focused component at least partly visible? |
| 2.4.13 Focus Appearance | AAA | Is the indicator large and contrasting enough between states? |
SC 2.4.7 requires that any keyboard operable interface has a mode of operation where the keyboard focus indicator is visible. No minimum thickness, no contrast ratio, no color specified. The indicator must exist and must not be time-limited while focus remains. User agents may hide focus rings for pointer users; authors must provide at least one mode—almost always keyboard navigation—where focus is visible. Common failures include outline: none on :focus without a replacement, focus ring color matching adjacent background so closely that practice defeats theory, and scripts that remove focus on the focus event. Passing 2.4.7 is necessary but not sufficient for usability.
SC 2.4.11 requires that when a user interface component receives keyboard focus, the component is not entirely hidden due to author-created content. Entirely hidden is the operative phrase. Partial obscuring is allowed at AA. A sticky footer covering the bottom half of a focused button fails. Covering the bottom edge might pass minimum but still harms usability. Typical author-created obscurers include fixed headers and footers, non-modal overlays that do not move focus, and sticky CTAs over form fields. Modal dialogs that trap focus generally pass because the focused element lives inside the dialog. Non-modal slide-out panels that let focus escape into obscured page content behind them are a common 2.4.11 failure. The criterion evaluates the component, not the focus ring alone, unless the ring is inside the component boundary.
SC 2.4.13 applies when the keyboard focus indicator is visible. An area of the indicator must meet both a minimum area rule and a change-of-contrast rule. Minimum area means at least as large as the area of a 2 CSS pixel thick perimeter of the unfocused component or sub-component. Change-of-contrast means at least 3:1 contrast ratio between the same pixels in the focused and unfocused states. This is not adjacent contrast of indicator versus background. It is change-of-contrast: compare pixel colors in focused state against the same pixel locations in unfocused state. A gray ring on white that was white before fails even if gray-on-white passes adjacent contrast. A solid 2px perimeter is the simplest pass. Inset outlines thinner than 2px fail the area calculation because inner perimeters shrink. Offset outlines still count at 2px thickness if the contrasting part meets 3:1 change. Only the portion of a gradient or dual-ring indicator that meets 3:1 change-of-contrast counts toward minimum area.
Token architecture for focus rings
Treat focus like any semantic color role. Do not hardcode a single blue on every component. A root-level token set defines ring color, offset background for two-color techniques, width, and offset. High-contrast mode overrides live beside the defaults. OKLCH for ring tokens helps because perceptual lightness makes it easier to pick a ring that contrasts against both near-white surfaces and mid-tone primary buttons without maintaining parallel hex tables per theme.
The :focus-visible pattern is the baseline, not bare :focus. Use :focus-visible so pointer clicks do not show a ring on every button while keyboard tabs do. Never remove :focus styling without a :focus-visible replacement. Mouse users on hybrid devices, speech-input users, and switch users may rely on visible focus without matching :focus-visible heuristics in every browser. A safe pattern sets outline: none only on :focus when :focus-visible supplies the indicator, and uses :focus:not(:focus-visible) { outline: none; } as progressive enhancement where supported.
:root {
--focus-ring-color: oklch(0.62 0.19 250);
--focus-ring-offset-bg: var(--surface-0);
--focus-ring-width: 2px;
--focus-ring-offset: 2px;
--focus-ring-color-hc: oklch(0.85 0.02 250);
}
:focus {
outline: none;
}
:focus-visible {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
}
Components on variable backgrounds—hero images, gradient cards, marketing sections with photography—need a two-color focus indicator: a dark outer ring and light inner ring or vice versa so at least one layer contrasts against any backdrop. A box-shadow halo simulates a second perimeter. Verify both layers. The outline must change at least 3:1 from unfocused pixels for 2.4.13. The shadow must be visible against adjacent page content for SC 1.4.11 Non-text Contrast on the indicator itself, which requires 3:1 against neighboring colors in the focused state.
:focus-visible {
outline: 2px solid var(--focus-ring-color);
outline-offset: 2px;
box-shadow: 0 0 0 4px var(--focus-ring-offset-bg);
}
Fixed chrome is a layout token problem as much as a color token problem. When focus moves, the browser scrolls the target into view inset by padding, revealing the component behind headers and footers.
html {
scroll-padding-top: 72px;
scroll-padding-bottom: 96px;
}
Match these values to measured fixed chrome height. Cookie banners that resize when dismissed need JavaScript updates or conservative padding. prefers-contrast: more deserves wider rings and higher lightness. forced-colors: active remaps author colors on Windows High Contrast Mode; aligning with system Highlight keeps focus visible where fighting brand blues fails.
Dark mode token pairs invert lightness strategy while keeping hue angle for brand consistency. Light mode rings often use saturated mid-chroma hue. Dark mode rings push lightness higher with slightly lower chroma. Re-run adjacent contrast per mode. A ring that passes on white at 4.5:1 may fail on dark navy at 2.8:1. Icon buttons on dark surfaces need visible rings without shrinking the icon. Primary-filled buttons need --focus-ring-on-primary distinct from the global ring. Links in body text benefit from outline with modest offset and border-radius. Multi-line links render outline per line fragment, which matches W3C perimeter interpretations more predictably than border on inline elements.
When you document focus tokens in a design system, export them through the DTCG 2025.10 stable format published October 2025 alongside surface and text tokens. Focus ring color, width, and offset are not afterthoughts in JSON. They are named tokens with test evidence in the same manifest as --text-muted, so Figma, CSS, and audit spreadsheets reference one source.
Measuring rings that hold up in audits
Adjacent contrast for AA evaluates the focus ring color against colors touching it in the focused state: page background outside an offset ring, button fill inside an inset ring. UI components require 3:1 minimum against adjacent colors per 1.4.11. Run pairs through a contrast checker. Test ring versus surface-0, ring versus surface-1 on cards, ring versus action-primary on filled buttons, and ring versus action-primary when focus sits on the border region versus offset outside.
Change-of-contrast for AAA 2.4.13 compares focused versus unfocused per indicator pixel. Screenshot or compute the button border color unfocused—transparent, a brand blue, a hairline gray. Focused state applies the ring color. Contrast ratio between those two must be at least 3:1. If unfocused state is transparent, the composited color against the background matters. Browsers may treat transparent as the background showing through. Test rendered results in Chrome and Firefox; do not assume.
Minimum area for 2.4.13 uses the unfocused component bounding box. For a 120 by 40 pixel button, W3C gives the 2px perimeter area formula as approximately 4h plus 4w, which yields 640 square pixels minimum indicator area. A 2px solid offset outline around the full bounding box meets this. A 1px outline does not. A box-shadow ring counts if the contrasting band is 2px thick and meets change-of-contrast.
Audit practice for 2.4.7 at AA means tab through every interactive element and confirm a ring or equivalent on each. No global outline: none without replacement. Custom widgets must forward focus to a visible child. Audit practice for 2.4.11 at AA means tab with sticky header and footer present and confirm the focused control is never one hundred percent hidden. Scroll-padding matches chrome. Non-modal overlays trap focus or dismiss on Escape. Audit practice for 2.4.13 at AAA when in scope means ring area at least equals the 2px perimeter equivalent and change-of-contrast at least 3:1 per indicator pixel. Dual-ring technique when backgrounds vary. Document results in the token pull request.
What not to do remains worth stating plainly. Global outline: none is the most common keyboard regression in component libraries. Assuming a subtle box-shadow alone satisfies 2.4.7 fails when the shadow is barely visible and change-of-contrast collapses. Testing only with a mouse never triggers obscuring failures pointers skip.
Case study: the audit that passed design and failed keyboard
A regional health nonprofit launched a redesigned patient portal after an external accessibility audit returned a clean AA report for the pages in scope. The report noted focus indicators were present. It did not test tab navigation with the live cookie consent banner, the fixed appointment shortcut bar, or the emergency alert ribbon that had been added after audit sign-off. Two weeks post-launch, a community member who navigates exclusively by keyboard filed a complaint through the patient advocacy channel. She could tab to the prescription refill link in the footer. She could not see it when focus arrived because the cookie banner covered every pixel of the control.
The engineering triage assumed a color problem first because design had recently shifted to a softer blue focus ring. They darkened the ring. The complainant replied that the ring was not the issue. The link was simply gone behind white UI chrome. That reframing sent the team to SC 2.4.11 instead of 2.4.7. Scroll-padding had never been set. The header height was 64 pixels but the cookie banner added another 88 when expanded. Focus moved correctly in the DOM. The viewport did not.
Fixing obscuring took one afternoon. Measuring it took a week because nobody had a keyboard test script that included sticky UI states. They added scroll-padding-top equal to header plus banner max height and scroll-padding-bottom for the mobile appointment bar. They wrote a Cypress spec that tabs to footer links with banners forced open. Regression caught a second failure: a slide-out help panel that did not trap focus let users tab into links visually behind the panel edge. That was another 2.4.11 pattern, not a ring color issue.
The ring color problem was real but separate. On navy primary buttons, the brand focus blue sat at roughly 2.1:1 adjacent contrast against the fill. The ring existed per 2.4.7. It failed 1.4.11 for non-text contrast of the indicator. They introduced --focus-ring-on-primary at higher lightness and re-tested. A designer asked whether they now passed AAA. They did not. Change-of-contrast between unfocused transparent border and the new ring met 3:1 on large buttons but failed on compact icon buttons where the 1px default border had been a mid-gray only 1.8:1 away from the ring hue. Thickening to 2px and shifting unfocused border to a darker transparent composite fixed the icon case.
The organization adopted 2.4.13 as an internal bar even though procurement only required AA. The policy change mattered for procurement language going forward: statements of work now named 2.4.7, 2.4.11, and 1.4.11 on indicators explicitly instead of saying visible focus in one clause. Focus tokens entered the DTCG export with contrast matrix attachments. The complainant received a direct response and agreed to retest. Her second session found the footer link scrolling above the banner with a visible two-color ring on the high-contrast theme she used.
The lesson the team repeats in onboarding is that success criterion numbers map to concrete fixes. 2.4.7 is existence. 2.4.11 is layout and scroll inset. 2.4.13 is geometry and change-of-contrast. 1.4.11 is adjacent contrast of the indicator in the focused state. Tokenize --focus-ring-color, --focus-ring-width, and --focus-ring-offset. Style :focus-visible. Add scroll-padding for fixed chrome. Measure adjacent contrast and change-of-contrast with evidence in the pull request. The numbers are not interchangeable. Conflating them is how teams ship rings that look fine in Figma and disappear in real sessions.