In CSS, what does :focus-visible match that :focus does not, and why was it added to the language?
answer
- all focus vs focus worth showing
- the heuristic asks about modality
- keyboard yes, mouse click usually no
- text fields always show it
- outline: none is the bug it replaced
basics
~20 s:focus matches any focused element; :focus-visible matches only when the browser judges a focus indicator should be shown, which in practice means keyboard focus and text-entry fields. It exists so removing mouse-click rings no longer strips keyboard users of their focus indicator.
solid answer
~50 s`:focus` matches an element whenever it holds focus, no matter how focus arrived — click, tap, script, or keyboard. `:focus-visible` matches only the subset where the user agent's heuristic says an indicator is warranted: focus moved by keyboard, and text-entry controls however they were focused. Browsers added it to end a long-standing conflict. Designers disliked the ring appearing after a mouse click, so codebases shipped `:focus { outline: none }` — which also erased the only cue keyboard users had. With `:focus-visible` you can style the two cases separately, and current browsers already draw their own default outline through `:focus-visible`, so leaving the UA styles alone is usually correct. The safe pattern is: never remove an indicator without replacing it, and attach your custom ring to `:focus-visible`, keeping `:focus` for effects that are fine on click too.
code
css · 19 lines.btn {
border: 1px solid #444;
}
/* keyboard focus: a clearly visible ring */
.btn:focus-visible {
outline: 3px solid #0b5fff;
outline-offset: 2px;
}
/* mouse/tap focus only: suppress it, fail-safe if unsupported */
.btn:focus:not(:focus-visible) {
outline: none;
}
/* highlight the whole group while any control inside has focus */
.field:focus-within {
border-color: #0b5fff;
}go deeper
Know that the focus ring is what keyboard users navigate by, and that custom focus styling belongs on :focus-visible rather than on :focus. Never write a blanket outline: none.
Explain the heuristic: keyboard focus matches, text fields match however they were focused, and a plain mouse click on a button usually does not. Say that current user-agent stylesheets already draw the default ring through :focus-visible.
Demonstrate the production instinct — audit for global outline resets, verify the indicator's contrast against both the component and the page, and test by tabbing rather than clicking, since a mouse pass hides the defect entirely.
Own it as a systemic guarantee: put the focus treatment in shared tokens or a base layer so no product team can reintroduce the reset, and treat an indicator regression as an accessibility-compliance failure rather than a cosmetic bug.
## The problem :focus-visible solves Before it existed there was exactly one focus selector, `:focus`, and it matched every focused element regardless of how focus arrived. That created a genuine conflict. A mouse user clicking a button already knows where they are, so the ring reads as visual noise; a keyboard user has no other cue at all, so the ring is the entire navigation model. Teams resolved the conflict the fast way — `:focus { outline: none }` or `*:focus { outline: 0 }` in a reset — and shipped a site that is unusable without a mouse. `:focus-visible` splits the case. It matches a focused element only when the user agent's heuristic concludes a visible indicator should be drawn. ## What the heuristic actually does The specification deliberately leaves the rule to the user agent, but the behaviour converged in practice: - Focus moved by keyboard — Tab, Shift+Tab, arrow keys inside a composite widget — matches `:focus-visible`. - Elements that accept text input match whenever they are focused, including by mouse. A caret sitting in a field with no ring is confusing, so the heuristic keeps the indicator. - A mouse press on a button or link generally focuses it (`:focus` matches) without `:focus-visible` matching. - Programmatic focus generally inherits the modality of the last interaction: after keyboard use, moving focus in script tends to match `:focus-visible`; after a click, it tends not to. Because it is a heuristic rather than a fixed rule, treat the first two bullets as reliable and the rest as "the browser decides". ## The modern default already uses it Current browsers' user-agent stylesheets draw the default focus ring via `:focus-visible`, not `:focus`. Two consequences follow. First, the old complaint that motivated `outline: none` has largely disappeared on its own — clicking a button in an unstyled page does not leave a ring. Second, `:focus { outline: none }` in your own stylesheet is now actively harmful, because it overrides a UA rule that was already doing the right thing. ```css /* Do not do this — it kills the keyboard indicator too */ button:focus { outline: none; } /* Do this — replace the ring, scoped to the case that needs it */ button:focus-visible { outline: 3px solid #0b5fff; outline-offset: 2px; } ``` `outline` is the right property for the job: it is drawn outside the box without participating in layout, so it never shifts anything, and in current browsers it follows `border-radius`. `outline-offset` pushes it clear of the element's own border. Using `box-shadow` instead works visually but disappears under forced-colors modes, so if you go that route add an `outline` fallback. ## The pattern for suppressing only the mouse ring If you must keep a custom ring but drop it on click, do not write a blanket removal. Scope the removal to the negation: ```css .btn:focus:not(:focus-visible) { outline: none; /* mouse/tap focus only */ } .btn:focus-visible { outline: 3px solid Highlight; outline-offset: 2px; } ``` The first rule is the fallback-safe order: if a browser did not support `:focus-visible`, the `:not()` would fail to parse and the removal would never apply, leaving the ring intact. That fail-safe direction is why this idiom persisted. ## :focus-within, the container case A related pseudo-class, `:focus-within`, matches an element that is focused **or** contains a focused descendant. It is how you highlight a whole form group, card, or combobox wrapper while any control inside it has focus, without script: ```css .field:focus-within { border-color: #0b5fff; } .field:focus-within > label { color: #0b5fff; } ``` Do not confuse the three: `:focus` is "this element has focus", `:focus-within` is "focus is somewhere in here", `:focus-visible` is "this element has focus and should show it". ## Interview-grade judgment The points that separate a strong answer: the indicator must meet a visible contrast bar against both the component and the page behind it; never rely on colour alone if the shape is unchanged; test by tabbing through the page, since a mouse pass will never reveal the bug; and remember that removing focus styles is a WCAG failure, not a taste decision. Browser support has not been a limitation since 2022 — `:focus-visible` shipped in Chrome 86, Firefox 85, and Safari 15.4 — so a codebase still carrying a global `outline: none` reset is carrying a defect, not a compatibility shim.
- Why is :focus:not(:focus-visible) preferred over a plain :focus { outline: none } plus a :focus-visible override?Order and failure mode. The `:not()` version removes the ring only in the case the browser judged safe, and in a browser that cannot parse `:focus-visible` the whole rule is dropped — so the ring survives. The blanket removal takes effect everywhere first and depends on a second rule winning the cascade to restore anything.
- Does a text input match :focus-visible when the user focuses it with a mouse click?Yes. The heuristic treats elements that accept text entry as always warranting an indicator, because a caret alone is a weak cue and the field may be one of several. That is why `:focus-visible` is not simply a synonym for "keyboard focus" — modality is only part of the rule.
- How would you highlight an entire form row while any control inside it is focused?Use `:focus-within` on the wrapper: it matches an element that is focused or contains a focused descendant, so `.field:focus-within { border-color: … }` lights up the row from the input, the select, or the button inside it. It updates automatically as focus moves and needs no script.
- What is the risk of replacing outline with box-shadow for the focus ring?`box-shadow` is suppressed in forced-colors and high-contrast modes, so the indicator can vanish exactly for the users who most depend on it, and unlike `outline` it is clipped by an ancestor's `overflow: hidden`. If the design needs a shadow, pair it with a transparent `outline` so a real outline still exists to be forced into view.
saying these in an interview costs you the question
- Calls :focus-visible a synonym for keyboard focus
- Removes outlines globally and calls it a design choice
- Thinks the UA default ring still comes from :focus
- Confuses :focus-visible with :focus-within
- Assumes a colour change alone is a sufficient indicator