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-visible heuristic 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.

Input modality routing to focus-visible A pointer click focuses without matching focus-visible, while keyboard focus matches focus-visible and shows a ring. When does :focus-visible match? mouse / touch click pointer Tab / arrow keys keyboard engine heuristic :focus matches no ring not :focus-visible ring shown :focus-visible

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 articles

More pages in the same section.