DEV Community

Cover image for The outline: none Debate Has Been Over Since 2020
Parsa Jiravand
Parsa Jiravand

Posted on Originally published at bestpractic.org

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.

*:focus {
  outline: none;
}
Enter fullscreen mode Exit fullscreen mode

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 fix people reach for next is also wrong

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:

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);
});
Enter fullscreen mode Exit fullscreen mode
body.using-mouse :focus {
  outline: none;
}
Enter fullscreen mode Exit fullscreen mode

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.

🎮 Try it yourself

▶️ Open the interactive playground →

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

The selector that made the JavaScript unnecessary

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.

.btn:focus {
  outline: none;
}
.btn:focus-visible {
  outline: 2px solid #4f8cff;
  outline-offset: 2px;
}
Enter fullscreen mode Exit fullscreen mode

: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.

The gotcha that trips people up on text fields

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.

🧠 Test yourself

Think it clicked? Take the 6-question quiz →

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

When outline: none alone is still fine

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:

Top comments (0)