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.7AAEN 301 549Section 508ADA 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).
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.
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
Search your CSS for outline: none and outline: 0.
Every match must have a visible replacement.
Style :focus-visible with a clear outline so keyboard users see
focus without a ring appearing on mouse clicks.
Keep a plain :focus ring as a fallback for browsers without
:focus-visible, then suppress it via
:focus:not(:focus-visible).
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.11AA2.4.13AAAEN 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.
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).
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.
Add outline-offset so the ring isn’t lost against the control’s
own border.
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.
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.11AAEN 301 549Section 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
Identify every fixed or sticky overlay — header, cookie bar, toolbar, chat
widget — and note its height.
Set scroll-padding-top on the scroll container (usually
html) to at least that height, so scrolled focus targets stop below
the overlay.
Alternatively, add scroll-margin-top to the interactive elements
themselves for finer control.
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).
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.