DevDockTools

:focus-visible vs :focus: Which One Should You Actually Use?

:focus fires on every focus event; :focus-visible only fires when the browser thinks a visible indicator is needed. Here's the concrete rule for choosing between them.

By Daniel Agrici5 min read
cssaccessibilityfocus-visiblefocusweb development

The Problem :focus-visible Was Built to Fix

For years, the default browser focus outline was ugly enough that teams reflexively wrote *:focus { outline: none; } and moved on. That single line is one of the most common accessibility regressions on the web — it removes the only visual signal keyboard users get for where they are on the page, with nothing put back in its place.

The tension: mouse users find a focus ring around a button they just clicked visually noisy and redundant. Keyboard users need that same ring to navigate at all. :focus alone can't distinguish between the two triggers — it fires identically whether focus arrived via Tab, a mouse click, or a programmatic .focus() call. :focus-visible exists specifically to solve that.

/* fires on every focus event, regardless of input method */
button:focus {
  outline: 2px solid blue;
}

/* fires only when the browser's heuristic decides a visible indicator is warranted */
button:focus-visible {
  outline: 2px solid blue;
}

How the Browser Decides

:focus-visible isn't purely "keyboard vs mouse" — it's a browser heuristic defined in the Selectors Level 4 spec that accounts for element type and interaction history. In practice, across current browser implementations:

  • Keyboard navigation (Tab, arrow keys on custom widgets) → :focus-visible matches.
  • Mouse click on a button or link → :focus-visible generally does not match (browsers assume the user doesn't need a persistent ring after a deliberate click).
  • Mouse click on a text input → :focus-visible typically does match, because focus indication is still useful there regardless of input method.
  • Programmatic .focus() calls → behavior follows the same heuristic as if focus had arrived via the "expected" method for that element, which varies by browser.

You don't need to special-case element types yourself — that judgment call is built into the pseudo-class. Your job is just deciding what to style.

The Standard Pattern

/* baseline: remove the default outline everywhere */
:focus {
  outline: none;
}

/* replace it, but only when the browser thinks it should show */
:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
}

This is the pattern most component libraries converged on: suppress :focus, restyle :focus-visible. It gives mouse users a clean click without a lingering ring, and keyboard users a clear, deliberately-styled indicator — not the browser's default blue glow, but something that matches your design system.

.btn:focus-visible {
  outline: 2px solid var(--focus-ring, #3b82f6);
  outline-offset: 2px;
  border-radius: 4px; /* match the element's own radius so the ring reads as intentional */
}

Pairing a subtle box-shadow ring with the outline is common for buttons with existing rounded corners or elevation — the Box Shadow Generator is useful for dialing in a focus ring's spread and color without it looking like an afterthought bolted onto the design.

Decision Point: When to Reach for Plain :focus Instead

:focus-visible is the right default for interactive controls, but it is not universally correct. Use plain :focus when:

  • The element is a text input, textarea, or contenteditable region. Users benefit from confirmation of exactly where their cursor landed regardless of how they got there — mouse-click focus on a text field genuinely needs a visible indicator, and most browser heuristics already agree (:focus-visible does match text inputs on click in current engines), but if you're overriding default behavior, don't suppress it for inputs.
  • You're building a custom widget where focus state carries semantic meaning beyond "here's where keyboard input goes” — e.g., a focused row in a data table that should look focused whether it got there by click or by arrow-key navigation, because the visual state communicates selection context, not just keyboard position.
  • You need guaranteed support in an environment where :focus-visible isn't reliable — extremely old browser targets, embedded webviews with outdated engines, or email-client-rendered HTML.
/* custom data-table row: focus state is informational, not just a keyboard cue */
.table-row:focus {
  background: var(--row-focus-bg);
}

Common Mistakes

Forgetting the fallback for outline: none

/* wrong: mouse-focus users lose ALL indication */
:focus {
  outline: none;
}
/* — no :focus-visible rule follows, so keyboard users lose it too */

If you write outline: none on :focus, you must pair it with a :focus-visible rule. Removing focus indication with nothing replacing it fails WCAG 2.4.7 (Focus Visible) and is one of the most common accessibility audit findings on production sites.

Styling :focus-visible with outline: 0 and nothing else

Some teams try to "clean up" focus rings by zeroing the outline and relying on a background-color change alone. That can work, but only if the alternative indicator has sufficient contrast and is distinguishable from hover state — a focus state that looks identical to :hover gives keyboard users no way to confirm focus actually moved.

Using :focus-within where you meant :focus-visible

These solve different problems. :focus-within matches a container when any descendant has focus — useful for highlighting a form field's wrapper when the input inside it is focused. It has nothing to do with input-method detection:

.form-group:focus-within {
  border-color: var(--focus-ring);
}

A Practical Default

For a new component library, start here and adjust per-component only where the decision points above apply:

*:focus {
  outline: none;
}

*:focus-visible {
  outline: 2px solid var(--focus-ring, #3b82f6);
  outline-offset: 2px;
}

/* text inputs keep a visible focus state regardless of trigger */
input:focus,
textarea:focus {
  outline: 2px solid var(--focus-ring, #3b82f6);
  outline-offset: 1px;
}

Test the result with an actual keyboard — tab through the page with the mouse untouched — before shipping. Browser heuristics are good but not infallible, and the only reliable check is watching whether the indicator you expect actually shows up at the moment a keyboard user needs it.

Frequently Asked Questions

Does :focus-visible remove focus outlines for mouse users?
It doesn't remove them outright — it lets the browser decide, using its own heuristic, whether the current focus event should show a visible indicator. In practice, browsers suppress it for most mouse clicks and show it for keyboard navigation, which is exactly the behavior most sites want.
Is it safe to replace all :focus styles with :focus-visible?
For interactive elements like buttons and links, generally yes. For form inputs, be more careful — some accessibility guidance favors keeping a visible focus indicator on text inputs regardless of input method, since users benefit from confirmation of where their cursor landed even after a mouse click.
What happens in browsers that don't support :focus-visible?
Support is broad across modern browsers, but if you need a fallback, feature-detect with @supports selector(:focus-visible) and provide a plain :focus rule outside that block as the baseline, so older browsers still get a visible indicator rather than none at all.