Visible focus & focus appearance

If you can’t see where the keyboard is, you can’t use it. For anyone who navigates with the Tab key — switch users, people with motor or vision disabilities, and plenty of keyboard-first power users — the focus indicator is the cursor. It is the one piece of feedback that says “you are here, press Enter and this is what happens.” Remove it, shrink it to a faint hairline, or let it slide under a sticky header, and the page becomes a guessing game: the user tabs into the void, loses their place, and activates the wrong control.

This lesson works through the three defects that account for most visible-focus failures. None of them needs JavaScript to fix; they come down to writing the right CSS for :focus-visible and making sure nothing in the layout swallows the control once it’s focused.

What you’ll learn

Why you must never remove the focus outline without giving back an equally clear one; how to size and colour an indicator so it meets the minimum-area and contrast thresholds of Focus Appearance; and how to keep a focused control fully visible — never tucked under a sticky header — so Focus Not Obscured is satisfied.

Standards this lesson maps to
Standard Criterion Level What it requires
WCAG 2.2 2.4.7 Focus Visible AA Any keyboard-operable interface has a mode of operation where the focus indicator is visible.
WCAG 2.2 1.4.11 Non-text Contrast AA The focus indicator and other UI components have at least a 3:1 contrast ratio against adjacent colours.
WCAG 2.2 2.4.11 Focus Not Obscured (Minimum) AA When a control receives focus, it is not entirely hidden by author-created content such as sticky headers.
WCAG 2.2 2.4.12 Focus Not Obscured (Enhanced) AAA When a control receives focus, no part of it is hidden by author-created content.
WCAG 2.2 2.4.13 Focus Appearance AAA The focus indicator meets minimum area, contrast, and is not fully hidden — a measurable strength bar.
EN 301 549 9.2.4.7 / 9.1.4.11 (incorporates WCAG) European harmonised standard; references the WCAG A/AA set including focus-visible and non-text contrast.
Section 508 502.3 / 504 (incorporates WCAG A & AA) US federal ICT must meet WCAG 2.0 Level A and AA, including a visible focus indicator.
ADA Title II WCAG 2.1 AA (DOJ rule) AA US state/local government web content must conform to WCAG 2.1 AA, including 2.4.7.

The three problems we’ll fix

Each card below isolates one common focus defect. For every issue you get a plain-language statement of the problem, a Bad example (shown as escaped, non-running code so it can’t harm this page), a Good example, the copyable Code, and an ordered fix.

Focus outline removed with no replacement

WCAG 2.2 · 2.4.7 AA EN 301 549 Section 508 ADA Title II

The single most damaging line in front-end CSS is *:focus { outline: none; }. It’s often added to kill the “ugly” default ring, but it strips the only signal a keyboard user has for where they are. Once it’s gone, tabbing through the page moves an invisible cursor — the user can’t tell which link or button is active, so they can’t know what Enter will do. The default outline isn’t sacred, but it must be replaced, not deleted. Modern CSS makes this easy: style :focus-visible so the indicator shows for keyboard users without putting a ring around every mouse click.

Bad

The outline is removed globally and nothing replaces it. Every keyboard user is now navigating blind (2.4.7).

bad-outline-none.css
/* Removes focus everywhere, gives nothing back */
*:focus {
  outline: none;
}

Good

If you must replace the default ring, do it on :focus-visible with a clearly visible outline. Keyboard users get a strong indicator; mouse users don’t see a ring on click.

good-focus-visible.css
:focus-visible {
  outline: 3px solid #1a73e8;
  outline-offset: 2px;
}

Code

A robust default: keep the native outline as a fallback for older browsers, then enhance for browsers that support :focus-visible. Never leave outline: none unpaired with a visible replacement.

focus-visible-progressive.css
/* Visible ring for everyone by default */
:focus {
  outline: 3px solid #1a73e8;
  outline-offset: 2px;
}

/* Where supported, suppress the ring only for non-keyboard focus */
:focus:not(:focus-visible) {
  outline: none;
}

How to fix

  1. Search your CSS for outline: none and outline: 0. Every match must have a visible replacement.
  2. Style :focus-visible with a clear outline so keyboard users see focus without a ring appearing on mouse clicks.
  3. Keep a plain :focus ring as a fallback for browsers without :focus-visible, then suppress it via :focus:not(:focus-visible).
  4. Tab through the whole page and confirm every interactive element shows a visible indicator (2.4.7).

Indicator too thin or too low-contrast

WCAG 2.2 · 1.4.11 AA 2.4.13 AAA EN 301 549

A focus ring that technically exists but is a 1 px pale-grey hairline is barely better than none. Two thresholds decide whether an indicator is strong enough. Non-text Contrast (1.4.11, AA) requires the indicator to have at least a 3:1 contrast ratio against the colours next to it — both the component it surrounds and the page behind it. Focus Appearance (2.4.13, AAA) adds a size floor: the indicator must cover an area at least equal to a 2 px-thick perimeter of the control (or a 4 px line along the shortest side) and have 3:1 contrast against the unfocused state. Thin, washed-out rings fail both — especially for low-vision users.

Bad

A 1 px light-grey outline against a white page falls well under 3:1 contrast and below the minimum thickness (1.4.11, 2.4.13).

bad-thin-ring.css
/* 1px and ~1.4:1 against white — fails contrast and area */
:focus-visible {
  outline: 1px solid #d0d0d0;
  outline-offset: 0;
}

Good

A solid 2 px (or thicker) outline in a colour with at least 3:1 contrast, plus an offset so it sits clear of the control’s own border.

good-strong-ring.css
:focus-visible {
  outline: 3px solid #0b5fff; /* ~5:1 on white */
  outline-offset: 2px;
}

Code

To stay visible on any background, pair a dark and light layer or add a contrasting box-shadow halo. This keeps 3:1 whether the control sits on white, on a photo, or on a dark theme.

dual-ring.css
:focus-visible {
  outline: 2px solid #ffffff;       /* light inner edge */
  outline-offset: 2px;
  box-shadow: 0 0 0 4px #0b1f44;    /* dark outer halo */
}

How to fix

  1. Make the indicator at least 2 px thick around the control (or a 4 px line on its shortest side) so it meets the area floor (2.4.13).
  2. Choose a colour with at least 3:1 contrast against both the control and the adjacent background (1.4.11). Check it with a contrast tool.
  3. Add outline-offset so the ring isn’t lost against the control’s own border.
  4. Test the indicator on every background it can appear over, including dark mode and high-contrast themes; use a dual light/dark ring if needed.
  5. Don’t rely on a colour change alone — a change of background colour with no outline often fails the area requirement.

Focused control hidden behind a sticky header

WCAG 2.2 · 2.4.11 AA EN 301 549 Section 508

Focus Not Obscured is new in WCAG 2.2 and catches a problem strong rings can’t solve. A position: sticky or fixed header (or a cookie bar, or a floating chat widget) overlays the top of the page. When a keyboard user tabs to a link that the browser scrolls just under that header, the control — and its focus ring — sit behind the overlay. At AA (2.4.11) the control must not be entirely hidden; at AAA (2.4.12) no part of it may be hidden. The browser scrolls the element to the very top edge, which is exactly where your fixed header is, so the default behaviour fails. The fix is pure CSS: reserve the header’s height with scroll-margin-top.

Bad

A 64 px fixed header with no scroll allowance. Tabbing to a target near the top scrolls it flush to the viewport edge — straight under the header (2.4.11).

bad-sticky-header.css
.site-header {
  position: fixed;
  inset-block-start: 0;
  height: 64px;
}
/* No scroll-margin: focused links land under the header */

Good

Give scrollable targets a scroll-margin-top equal to (or a little more than) the header height. The browser now stops scrolling before the control reaches the overlay, so focus stays fully visible.

good-scroll-margin.css
:root {
  --header-h: 64px;
}
/* Keep focused/scrolled targets clear of the fixed header */
a, button, input, select, textarea, [tabindex] {
  scroll-margin-top: calc(var(--header-h) + 8px);
}

Code

For AAA (2.4.12) ensure no part is covered. A reliable approach is to reserve the space on the scroll container itself so the overlay never sits over content the user can land on.

scroll-padding.css
/* Reserve header height for ALL programmatic scrolling */
html {
  scroll-padding-top: 72px; /* >= header height */
}

How to fix

  1. Identify every fixed or sticky overlay — header, cookie bar, toolbar, chat widget — and note its height.
  2. Set scroll-padding-top on the scroll container (usually html) to at least that height, so scrolled focus targets stop below the overlay.
  3. Alternatively, add scroll-margin-top to the interactive elements themselves for finer control.
  4. Tab through the page with the overlay present and confirm each focused control is fully visible, not just partly (aim for 2.4.12 AAA).
  5. Re-check after the overlay changes height (e.g. a banner that wraps on narrow screens) and tie the value to a single CSS variable.

Recap

  • Never ship outline: none on its own. If you remove the default outline, replace it with an equally clear indicator on :focus-visible (2.4.7).
  • Make the indicator strong: at least a 2 px-thick perimeter (or equivalent area) and a 3:1 contrast ratio against both the component and the background (1.4.11, 2.4.13).
  • Keep the focused control fully on screen. Reserve space for sticky headers with scroll-margin-top so focus never disappears beneath them (2.4.11).

The same CSS satisfies WCAG, EN 301 549, Section 508, and ADA Title II at once — give focus a strong, unobstructed indicator and you meet them all.