What is the difference between :focus, :focus-within, and :focus-visible?
Advertisement
🧩 Scenario
Architecture Walkthrough
:focus Matches Whenever the Element Has Focus
:focus matches the element that currently holds focus, regardless of how focus arrived: keyboard navigation, a mouse click, a touch, or a programmatic element.focus() call.
That breadth is its problem. Because it matches on mouse click too, a visible focus ring appears when a user clicks a button, which most designs do not want. The historic response was outline: none on :focus, which removed the indicator for everyone, including keyboard users who have no other way to know where they are.
:focus-visible Matches When the Indicator Should Show
:focus-visible matches only when the browser determines that a focus indicator ought to be visible. It exists specifically to resolve the tension above: keep the ring for users who need it, suppress it for users who do not.
The determination is a heuristic based on input modality and element type, not a simple keyboard-versus-mouse test. Roughly:
- Keyboard navigation always matches.
- Mouse clicks on a button generally do not match.
- Text inputs and textareas match regardless of modality, because a user about to type needs to know where the caret is.
- Programmatic focus matches if the most recent interaction was keyboard-driven, which is what makes focus management in modals and menus behave sensibly.
That element-type dependence is the part people miss. :focus-visible is not "the keyboard-only pseudo-class"; it is "the browser's judgement about whether an indicator is warranted", and the browser judges text entry differently from activation.
Browser default styles now use :focus-visible for the built-in outline, which is why unstyled buttons no longer show a ring on click in modern browsers.
:focus-within Matches an Ancestor
:focus-within matches an element if it, or any descendant at any depth, has focus. It is not limited to the immediate parent.
This is what lets a container react to focus inside it: a search bar wrapper that highlights when its input is focused, a card that raises when any of its interactive elements gains focus, a fieldset that outlines when any control inside it is active, a dropdown that stays open while focus remains anywhere within it.
Before it existed, all of those required focusin and focusout listeners maintaining a class on the container, with the usual synchronisation bugs. Note that focus does not bubble but focusin does, which is why the JavaScript version was subtly harder than it looked, and :focus-within removes the problem entirely.
Why outline: none Is a Failure
WCAG 2.4.7 requires a visible focus indicator, and WCAG 2.4.11 in the 2.2 revision adds minimum appearance requirements for it. Removing the outline without providing an alternative fails both, and it is one of the most frequently cited accessibility defects in audits.
The correct pattern is either to keep the default outline, which is now :focus-visible-gated by browsers anyway, or to replace it with an equally visible custom indicator on :focus-visible.
Two details matter when replacing it. outline-offset is worth using so the ring sits clear of the element's own border rather than on top of it, which improves visibility on dense layouts. And in forced-colors mode, used by Windows High Contrast users, box-shadow and background-color are stripped while outline is preserved, so an indicator built purely from box-shadow disappears for exactly the users most likely to need it. Keeping a transparent outline in the base state and colouring it on focus is the pattern that survives forced-colors mode.
Combining the Three
The three compose naturally: :focus-visible for the element's own indicator, :focus-within for the container's response, and :focus reserved for the rare case where an indicator genuinely should appear regardless of modality, such as a custom text-entry widget the heuristic does not recognise.
A common safe reset removes the default outline only where a replacement is provided, and never removes it unconditionally.
Key Code Explained
/* :focus — matches on keyboard, mouse, touch, and programmatic focus */
.button:focus {
outline: 2px solid #3b82f6; /* also appears on mouse click */
}
/* THE ACCESSIBILITY FAILURE: removes the indicator for EVERYONE */
.button:focus {
outline: none; /* keyboard users can no longer tell where they are */
}
/* :focus-visible — the browser decides whether an indicator is warranted */
.button:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px; /* sits clear of the element's own border */
}
/* The safe reset: suppress the default ONLY where a replacement exists */
.button:focus:not(:focus-visible) {
outline: none;
}
/* Forced-colors safe: outline survives, box-shadow does not */
.input {
outline: 2px solid transparent; /* reserved in the base state */
outline-offset: 2px;
transition: outline-color 150ms ease;
}
.input:focus-visible {
outline-color: #3b82f6;
}
/* A box-shadow-only ring vanishes in Windows High Contrast mode */
.input-fragile:focus-visible {
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.4); /* stripped in forced-colors */
}
/* :focus-within — matches an ancestor when ANY descendant has focus */
.search-bar {
border: 1px solid #cbd5e1;
border-radius: 8px;
}
.search-bar:focus-within {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.15);
}
/* Works at any depth, not just for a direct child */
/* Keep a menu open while focus stays anywhere inside it */
.menu:focus-within .menu__panel {
display: block;
}
/* Raise a card when any interactive element inside it gains focus */
.card:focus-within {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
}
/* Text inputs match :focus-visible even on mouse click,
because typing requires knowing the caret location */
textarea:focus-visible {
outline: 2px solid #3b82f6; /* shows regardless of modality */
}
/* Combining all three coherently */
.field:focus-within { border-color: #3b82f6; } /* container reacts */
.field input:focus-visible { outline: 2px solid #3b82f6; } /* element indicator */
The three .button rules at the top tell the whole story in sequence: :focus shows the ring too often, outline: none shows it never, and :focus-visible shows it exactly when it is warranted. Being able to lay out that progression is a better answer than defining the three pseudo-classes in isolation, because it explains why the third one was added to the language.
The forced-colors block is the detail that distinguishes a thorough answer. A box-shadow focus ring looks excellent, passes a casual accessibility review, and disappears entirely for Windows High Contrast users, who are disproportionately likely to be keyboard-dependent. Reserving a transparent outline in the base state and colouring it on focus keeps the indicator in every mode and animates cleanly.
Tradeoffs
| Pseudo-class | Matches | Use for | Risk |
|---|---|---|---|
:focus | Any focus, any modality | Custom text-entry widgets the heuristic misses | Ring appears on mouse click |
:focus-visible | When the browser judges an indicator warranted | Element focus indicators, almost always | Behaviour varies by element type |
:focus-within | Element or any descendant focused | Containers, wrappers, keeping menus open | Matches at any depth, so scope carefully |
outline: none alone | N/A | Nothing | Fails WCAG 2.4.7 |
What Interviewers Actually Check
- Whether you can state what each of the three matches
- Whether you know
:focus-visibleuses a heuristic that also depends on element type - Whether you frame
outline: noneas an accessibility failure and know the safe pattern - Whether you can use
:focus-withinfor a container and know it works at any depth - Whether you know
box-shadowindicators disappear in forced-colors mode
Follow-Up Questions
- What exactly does WCAG 2.4.11 add to the focus indicator requirements beyond visibility?
- Why does
focusnot bubble whilefocusindoes, and how did that complicate the pre-:focus-withinJavaScript approach? - What does forced-colors mode strip and preserve, and how would you test a focus style against it?
- How should focus be managed when a modal opens and closes, and how does
:focus-visiblerespond to programmatic focus? - What is the difference between
:focus-withinand:has(:focus), and is there any reason to prefer one?
Common Candidate Mistakes
- Writing
outline: nonewith no replacement indicator, which removes the only cue keyboard users have about their position and fails WCAG 2.4.7 - Styling
:focuswhen:focus-visiblewas intended, so the focus ring appears on every mouse click and designers push back on having any indicator at all - Assuming
:focus-withinmatches only a direct parent, when it matches any ancestor of the focused element at any depth - Building a focus ring from
box-shadowalone, which is stripped in forced-colors mode and disappears for high-contrast users - Describing
:focus-visibleas keyboard-only, when the heuristic also depends on element type and shows the indicator for text inputs regardless of how focus arrived
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you state what each of the three pseudo-classes matches?
- Can you explain the heuristic
:focus-visibleuses, including its element-type dependence? - Can you write a focus style that keeps keyboard users covered while suppressing the ring on mouse click?
- Can you use
:focus-withinto style a container from a descendant's state? - Can you explain why
outlinewithoutline-offsetis preferable to abox-shadow-only indicator?
Summary
:focus matches whenever an element holds focus, regardless of how it arrived, which is why it shows a ring on mouse clicks and why the historic response was outline: none, removing the indicator for keyboard users who depend on it. :focus-visible matches only when the browser judges that an indicator is warranted, using a heuristic based on input modality and element type: keyboard navigation always matches, mouse clicks on buttons generally do not, and text inputs match regardless because a user about to type needs to see the caret location. :focus-within matches an element when it or any descendant at any depth has focus, which lets containers react to focus inside them.
The accessibility framing matters. WCAG requires a visible focus indicator, so outline: none without a replacement is a genuine failure rather than a style choice. The correct pattern is either to keep the browser default, which is already :focus-visible-gated in modern engines, or to provide a custom indicator on :focus-visible with outline-offset so it sits clear of the element's border.
One implementation detail is easy to miss: forced-colors mode strips box-shadow and background-color while preserving outline, so a shadow-based focus ring vanishes for high-contrast users who are disproportionately keyboard-dependent. Reserving a transparent outline in the base state and animating outline-color on :focus-visible produces an indicator that survives every rendering mode. Used together, :focus-visible handles the element's own indicator, :focus-within handles the container's response, and plain :focus is reserved for the rare widget the heuristic does not recognise.
Why not just remove the focus outline if it looks bad on click?
Because keyboard users depend on it to know where they are. outline: none is one of the most common accessibility failures. :focus-visible exists precisely so you can keep the indicator for keyboard users and suppress it for mouse users.
Does :focus-visible work on text inputs clicked with a mouse?
Yes. Browsers show the focus indicator for text inputs regardless of input modality, because typing requires knowing where the caret is. The heuristic is not purely keyboard versus mouse.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement