vs :focus: Why It Replaced the Focus-Ring Polyfill
For years the standard advice — "never remove the focus outline" — collided with a real design grievance: mouse users saw a ring snap around every button they clicked, and designers responded by stripping outline: none site-wide, quietly breaking keyboard navigation. The narrow problem this guide solves is showing a focus indicator only to people who need it (keyboard and other non-pointer users) without hiding it from anyone, and doing so with the native :focus-visible pseudo-class rather than the JavaScript focus-ring polyfill that used to be required. This page belongs to Hover & Focus State Design, and it builds directly on the focus-indicator work in Creating Accessible Focus Indicators.
What this page settles:
- Exactly how the
:focus-visibleheuristic differs from:focus - Why the WICG focus-ring polyfill is now obsolete
- A modern fallback that degrades safely on legacy engines
- How to never trip WCAG 2.4.7 while pleasing designers
The old problem and the polyfill that filled the gap
Before :focus-visible, CSS had only :focus, which fires for any focus regardless of how it arrived. Click a button with a mouse and :focus matches; tab to it with a keyboard and :focus matches identically. Designers who disliked the click-ring removed it globally — and removed it for keyboard users too, the exact people the ring exists for. The community's answer was the WICG focus-visible polyfill: a script that watched input modality, decided whether the most recent interaction was keyboard-like, and toggled a .focus-visible class you could style. It worked, but it was JavaScript on the critical path solving a presentation problem, with a flash of unstyled focus before the script booted.
The platform absorbed that heuristic into the engine. :focus-visible is a pseudo-class the browser matches using the same modality reasoning the polyfill performed, but natively, synchronously, and with no script to load. The diagram below contrasts the two input paths and shows where :focus-visible chooses to match.
Complete working implementation
The pattern has three layers: kill the legacy click-ring, restore a strong ring for :focus-visible, and leave a :focus fallback for any engine that does not understand the newer pseudo-class. Order matters.
<nav class="bar">
<button type="button" class="btn">Save</button>
<a class="btn" href="#next">Continue</a>
<input class="field" type="text" aria-label="Search">
</nav>
:root {
--ring-color: #2d5bff;
--ring-width: 3px;
}
/* 1. Fallback for engines without :focus-visible — keep SOME ring so we
never ship an outline-less keyboard experience to old browsers. */
.btn:focus {
outline: var(--ring-width) solid var(--ring-color);
outline-offset: 2px;
}
/* 2. In engines that DO support :focus-visible, remove the ring for the
cases the heuristic says don't need it (e.g. mouse clicks)... */
.btn:focus:not(:focus-visible) {
outline: none;
}
/* 3. ...and apply the strong ring only when the heuristic asks for it. */
.btn:focus-visible {
outline: var(--ring-width) solid var(--ring-color);
outline-offset: 2px;
/* A second offset ring guarantees contrast on any background. */
box-shadow: 0 0 0 calc(var(--ring-width) + 2px) #fff;
}
/* Text inputs benefit from a visible ring even on click, which the
heuristic already grants — no special-casing needed. */
.field:focus-visible {
outline: var(--ring-width) solid var(--ring-color);
}
The key is rule 2. A browser that does not recognise :focus-visible treats the whole selector :focus:not(:focus-visible) as invalid and discards it, so the legacy ring from rule 1 stays. A browser that does recognise it evaluates the selector, suppresses the ring on pointer focus, and lets rule 3 paint the keyboard ring. You get correct behaviour on both classes of engine from one stylesheet.
The technique that makes it work
The load-bearing selector is :focus:not(:focus-visible). It means "focused, but not in a way the engine considers worth indicating" — precisely the mouse-click case designers object to. Removing the outline only inside that narrow selector is what lets you delete the click-ring without ever touching the keyboard ring. This is the entire reason the JavaScript polyfill became unnecessary: the modality detection that the polyfill computed in script is now expressed declaratively, and :not() gives you a forward-compatible way to apply the cleanup only where the new pseudo-class exists.
Variation: high-contrast and forced-colors
Some users run a forced-colors mode (Windows High Contrast). In that mode the system overrides your colours, and an outline survives where a box-shadow ring may not. Keep the outline as the primary indicator and let the system theme it.
@media (forced-colors: active) {
.btn:focus-visible {
/* Use the system's highlight colour; box-shadow is ignored here. */
outline: var(--ring-width) solid Highlight;
box-shadow: none;
}
}
For dark themes, swap the offset ring colour so the halo stays visible:
@media (prefers-color-scheme: dark) {
.btn:focus-visible { box-shadow: 0 0 0 calc(var(--ring-width) + 2px) #11151c; }
}
Browser support
:focus-visible is supported natively in Chrome 86+, Edge 86+, Firefox 85+, and Safari 15.4+, which has covered all evergreen browsers since 2021 or 2022. The :focus fallback in the implementation handles anything older. Because the cleanup selector :focus:not(:focus-visible) is parse-discarded by unsupporting engines, you do not need an @supports selector(:focus-visible) wrapper — the cascade handles the fallback for you.
FAQ
What is the difference between :focus and :focus-visible?:focus matches any focused element regardless of input device, so it shows a ring even on mouse clicks. :focus-visible matches only when the browser heuristic decides a visible indicator is useful, which is essentially keyboard and other non-pointer focus.
Do I still need the focus-visible polyfill in 2026?
No. Native :focus-visible is supported in every evergreen browser since 2021, so the WICG focus-visible JavaScript polyfill is obsolete. Use the native pseudo-class with a plain :focus fallback for very old engines.
Should I ever remove the focus outline entirely?
Never remove a focus indicator without replacing it with an equally visible one. Removing :focus outlines breaks keyboard navigation and fails WCAG 2.4.7. Scope your custom ring to :focus-visible instead.
Why does :focus-visible sometimes show a ring after a mouse click?
The heuristic shows the ring when the element was focused programmatically or is a text input, since those benefit from a visible caret context. For ordinary buttons, a mouse click suppresses the ring.
Related
- Hover & Focus State Design — the parent guide on interactive state styling.
- Creating Accessible Focus Indicators — contrast and sizing requirements for the ring itself.
- Accessible CSS-Only Tooltips — pairs focusable triggers with focus-revealed content.
- Smooth Hover Effects Without JavaScript — keeping hover and focus visuals in parity.
- Feature Detection With @supports — the
@supports selector()form for guarding newer selectors.
Related articles
More pages in the same section.