Articles

CSS :focus-visible vs :focus Explained

:focus-visible only shows a focus ring for keyboard and other non-pointer input, while :focus matches every focus event, including mouse clicks.

Takina Takina · · 4 min read
Close-up of CSS code on a screen

:focus-visible is a CSS pseudo-class that matches an element only when the browser decides a visible focus indicator would help the user — typically after keyboard navigation — while its older sibling :focus matches any focus event, including a mouse click. The difference solves a long-standing tension in web design: keyboard and screen-reader users need a visible focus ring to know where they are, but that same ring showing up after every mouse click looks broken to sighted mouse users.

Why :focus alone caused problems

For years, developers had two bad options when styling focus states with plain :focus:

  1. Leave the browser’s default focus ring in place, which is functional but often visually inconsistent with the rest of the design and shows up on every click.
  2. Suppress it with outline: none, which fixes the visual complaint but breaks keyboard navigation for everyone who relies on it — a widely cited accessibility failure.

Neither option is good. Removing outlines without a replacement is one of the most common accessibility regressions on the web, because it looks fine to whoever tested with a mouse and is invisible to them that keyboard users lost their bearings entirely.

What :focus-visible actually checks

:focus-visible uses browser heuristics, not a simple “was this triggered by keyboard” rule, though keyboard input is the dominant case. Roughly:

  • A <button> or <a> focused by clicking with a mouse: does not match :focus-visible (a click already tells the user where their attention is).
  • The same element focused by pressing Tab: matches :focus-visible.
  • A text <input> focused by clicking: matches :focus-visible — because even a mouse click leaves ambiguity about where the caret landed, so the browser still shows the ring.
  • Programmatic focus via element.focus() in JavaScript: behavior depends on context, generally following whether the action was a response to keyboard interaction.

The exact heuristic is left to each browser engine and refined over time, which is the tradeoff for not having to write your own input-method detection in JavaScript.

Basic usage

/* Remove the ring for pointer interactions that don't need it */
button:focus:not(:focus-visible) {
  outline: none;
}

/* Style the ring for interactions that do need it */
button:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

A cleaner pattern skips the negation entirely:

button {
  outline: none;
}

button:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

This keeps default styling clean while restoring a visible ring specifically for the users who need it, without touching JavaScript.

Browser support and the polyfill era

:focus-visible shipped natively in every major browser engine, but for a stretch of time it existed only as a draft spec with a JavaScript polyfill filling the gap — a library that added a .focus-visible class to elements by inferring keyboard-vs-pointer usage manually, then let CSS target that class instead of a native pseudo-class. That polyfill still shows up in older codebases and component libraries that haven’t been cleaned up since. If you’re maintaining a project that predates broad native support, it’s worth checking whether the polyfill is still bundled and can simply be dropped in favor of the native selector — one less dependency shipped to every visitor, and one less place for the keyboard/mouse heuristic to disagree with what the browser itself would decide.

:focus vs :focus-visible

:focus:focus-visible
Matches mouse-click focusYesUsually no (browser heuristic)
Matches keyboard (Tab) focusYesYes
Matches text input click focusYesYes
Requires JavaScript to fakeNoNo
Good default for buttons/linksRing shows on every clickRing shows only when helpful
Good default for text inputsFine either wayFine either way

Where it fits with other modern selectors

:focus-visible pairs naturally with other relational and logical CSS selectors that shipped around the same time, like :has() and :is()/:where(), all part of the same push toward expressing more UI logic directly in CSS instead of JavaScript. If you’re layering focus styles with cascade layers or custom properties for theming, keep in mind that :focus-visible on its own doesn’t change specificity rules — see what CSS specificity is if your focus ring is getting overridden unexpectedly by another rule.

A note on :focus-within

Don’t confuse :focus-visible with :focus-within, a different pseudo-class that matches a container when any descendant has focus — useful for highlighting a whole form row when its input is focused, regardless of how that focus was triggered. The two solve different problems and combine well: :focus-within picks the container, :focus-visible decides whether the indicator inside it should be visible.

The takeaway

:focus matches any focus event; :focus-visible adds browser-level judgment about whether a visible indicator is actually useful right now, hiding the ring after a confident mouse click while keeping it for keyboard navigation and other ambiguous cases. Reach for :focus-visible as the default on interactive elements — it gets you the design polish that used to require suppressing outlines entirely, without the accessibility regression that suppression caused for years.

Takina Takina · · 4 min read

CSS object-fit and object-position, Explained

object-fit controls how an image or video is cropped inside its box, and object-position controls which part of it stays visible. How they work together.

#CSS #Web Development #Frontend
Takina Takina · · 5 min read

CSS inherit, initial, unset & revert Explained

CSS's four global keywords control where a property's value comes from. How inherit, initial, unset, and revert differ, with a comparison table.

#CSS #Web Development #Frontend
Takina Takina · · 4 min read

Dynamic Viewport Units: dvh, svh, and lvh Explained

dvh, svh, and lvh fix the classic mobile vh bug where browser toolbars cut off full-height layouts. Here's what each unit measures and when to use it.

#CSS #Web Development #Frontend