← blog
CSSAugust 29, 2026 · 4 min read

The outline: none Debate Has Been Over Since 2020

A designer flags the ugly blue focus ring in review. An engineer deletes it with outline: none. Nobody notices the keyboard-only user who now can't tell where they are on the page.

Parsa Jiravand · Frontend engineer · building bestpractic
The outline: none Debate Has Been Over Since 2020

A design review flags it every time: a button gets clicked, and a chunky blue ring slaps itself around the edge for half a second. "Can we remove that? It looks broken." The engineer knows the one-line fix.

CSS
1
2
3
*:focus { outline: none; }

Ship it. The review passes. The button looks clean in every screenshot, in every browser, on every click.

Three weeks later an accessibility audit comes back with a line item: "Keyboard users cannot determine which element currently has focus." Nobody touched a mouse to find that bug. They tabbed through the page with their eyes closed to the cursor, because that's how they use the page every day — and your clean button is now indistinguishable from every other pixel on the screen.

That's not a nitpick. It's WCAG 2.4.7, a real success criterion, and outline: none on its own fails it every time.

The instinct isn't to put the ring back — nobody wants the ugly version either. It's to make it conditional: show it for keyboard users, hide it for mouse users, since a mouse click already gives you visual feedback that something happened. Reasonable goal. Before 2020, CSS had no way to ask "was this focus event a click or a keypress?" — so people answered the question in JavaScript instead:

JavaScript
1
2
3
4
5
6
7
8
let usingMouse = false; document.addEventListener("mousedown", () => { usingMouse = true; }); document.addEventListener("keydown", (e) => { if (e.key === "Tab") usingMouse = false; }); document.addEventListener("focusin", (e) => { document.body.classList.toggle("using-mouse", usingMouse); });
CSS
1
2
3
body.using-mouse :focus { outline: none; }

This is roughly what the WICG's own focus-visible polyfill did under the hood, and plenty of design systems shipped a version of it. It worked. It also meant every page paid for a global event-listener setup, a class toggling on <body> on every focus change, and a heuristic that could still drift out of sync — tab into a field, click somewhere unrelated, tab again, and the class-based guess doesn't always match what actually happened.

Runs right in your browser — poke at it and watch the concept react live.

Browsers already knew whether a focus event came from a keyboard or a pointer — they use that exact signal internally to decide their own default outline behavior. :focus-visible just exposes it to your CSS.

CSS
1
2
3
4
5
6
7
.btn:focus { outline: none; } .btn:focus-visible { outline: 2px solid #4f8cff; outline-offset: 2px; }

:focus still matches on every focus event, same as always — a mouse click, a tap, a keyboard tab, a .focus() call from your own JS. :focus-visible only matches the subset of those where the browser's own heuristic decides a visible indicator is warranted. Tab to the button: it matches, the ring shows. Click the button with a mouse: it doesn't, no ring, and no JavaScript had to watch for either.

It shipped in Chrome and Edge in October 2020 (Chrome 86), and the rest followed within about two years — by 2022 it worked in every evergreen browser without a flag or a polyfill.

Try the pattern above on a <button> and a mouse click gives you no ring, exactly as expected. Try it on a text <input> and clicking it still shows the ring, even with your mouse.

That's not a bug in your CSS — the browser's heuristic treats form fields you're about to type into differently from buttons you just clicked. A button click is a complete action; you got your feedback from the click itself. A text field click is the start of typing, and you still need to see that the field is focused so you know where the caret is. :focus-visible accounts for that per element, not just per input device — which is exactly the nuance the old body.using-mouse class-toggle approach couldn't express, because it only ever knew about the page as a whole.

Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.

There's one case where deleting the ring outright is the right call, not a shortcut: an element that's tabindex="-1" and only ever receives programmatic focus your own script manages — a modal you focus on open, then visibly indicate some other way (a border, a heading, an announced live region). If a human never tabs to it and never clicks it, there's no keyboard user to leave stranded. Everywhere a real person can land with Tab, :focus-visible is the answer, not a suppressed outline and a hope that nobody notices.

Go find the *:focus { outline: none; } line in whatever you're shipping — or the using-mouse class hack if your codebase predates 2021 — and swap it for the two rules above. Which one does your codebase still have?


🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.

Thanks for reading! Let's stay connected:

Keep reading

One post a day, in your inbox

Each one with a runnable playground and a quiz. No pitch, no digest, unsubscribe in one click.

0 comments

Sign in to join the discussion, like comments, and save articles for later.