The accessible way to handle focus rings in 2026 is :focus-visible. It applies the outline only when the browser decides the user is navigating by keyboard, so the ring shows up for someone tabbing through a form but stays out of the way when someone clicks a button with a mouse. That single pseudo-class is what finally lets you stop blanket-removing outlines, which is the habit that quietly broke keyboard accessibility on a generation of sites.
/* Drop the default ring only for the cases :focus-visible would NOT catch
(mouse clicks), and keep a clear ring for keyboard focus. */
:focus:not(:focus-visible) {
outline: none;
}
:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}That is the whole pattern. The rest of this page is why it works, the trap it replaces, and how to draw a ring that actually passes an audit.
The problem :focus-visible solves
The plain :focus pseudo-class fires on every focus, however it happened: keyboard tab, script call, and a plain mouse click on a button or link. Browsers paint a default focus ring on that state, which is correct and necessary for keyboard users. The friction is that a lot of designers and clients see the ring appear on a mouse click, decide it looks like a bug, and reach for the nuclear option:
/* Do NOT do this. */
:focus {
outline: none;
}That removes the focus indicator for everyone, including the keyboard user who now has no idea which element is focused. It is one of the most common accessibility regressions on the web, and it directly fails WCAG 2.4.7 Focus Visible (Level AA), which requires a visible keyboard focus indicator.
For years the workaround was a JavaScript polyfill (focus-visible.js, which added a .focus-visible class, plus the Firefox-only :-moz-focusring prefix) that tried to guess keyboard versus pointer focus. You can delete all of that now. :focus-visible is native, it is the heuristic done properly by the browser itself, and the polyfill and the -moz-focusring prefix are both obsolete.
How the browser decides what is "visible"
:focus-visible matches when an element is focused and the user agent's heuristic decides the focus should be made evident. You do not control the heuristic, and that is the point: the browser has far better signal about input modality than your CSS does. In practice the rules shake out roughly like this:
- Keyboard focus (Tab, Shift+Tab, arrow keys) matches
:focus-visible. The ring shows. - Mouse or touch focus on a button or link does not match. No ring.
- Text inputs and
textareaalways match, even on a mouse click, because a blinking caret is not enough of a signal that the field is focused. - Script-driven
element.focus()matches if the last interaction was via keyboard, and the browser also matches it when there was no prior interaction (so a focus set on page load is still indicated).
You will not memorize the exact matrix and you do not need to. Style :focus-visible and trust the engine to apply it at the right moments.
The modern pattern, and the one not to use
There are two clean ways to wire this, and one stale way that lingers in old tutorials.
The version I prefer keeps the browser's default ring as a baseline and only suppresses it for the non-keyboard case:
/* Remove the ring only where :focus-visible would not have shown it anyway. */
:focus:not(:focus-visible) {
outline: none;
}
:focus-visible {
outline: 2px solid currentColor;
outline-offset: 3px;
border-radius: 2px;
}The shorter version, if you are happy to define your own ring everywhere, is to never touch :focus at all and only ever style :focus-visible:
:focus-visible {
outline: 3px solid #1d4ed8;
outline-offset: 2px;
}What you should not write is the old :focus reset that strips the outline for everyone and then tries to re-add it. If you remove the outline on :focus unconditionally, you have already broken the keyboard experience for any browser or moment where :focus-visible does not re-apply it. Suppress on :focus:not(:focus-visible) or do not suppress at all.
Draw a ring worth showing
Hiding the ring for mouse users is only half the job. The ring keyboard users do see has to be good, because WCAG 2.4.11 Focus Not Obscured (new in WCAG 2.2, Level AA) now also requires that the focused element is not entirely hidden behind sticky headers or other content. A few rules I hold to:
- Use
outline, notbox-shadowor aborder, for the ring.outlinedoes not affect layout, so adding it on focus never shifts the page. Aborderchange does, which causes a visible jump. - Always pair it with
outline-offset. A couple of pixels of offset lifts the ring off the element so it reads as a ring and not a thick edge. It also helps the ring stay visible against the element's own background. - Never ship
outline: 0oroutline: noneas the only focus style. If you remove the default outline, you owe the page a replacement that is at least as visible. - Give the ring contrast against both light and dark backgrounds.
currentColoris a cheap way to track the element's text color; an explicit high-contrast color is safer on busy backgrounds.
.button:focus-visible {
outline: 2px solid #ffffff;
outline-offset: 2px;
/* A second ring via box-shadow guarantees contrast on any button color. */
box-shadow: 0 0 0 4px #1d4ed8;
}If you want to tint the native controls (checkboxes, radios, range sliders) that sit alongside these focusable elements, CSS accent-color is the companion property, and it cooperates with :focus-visible rings cleanly.
Browser support
:focus-visible is Baseline and safe to use in production with no fallback. Chrome and Edge shipped it in version 86 (October 2020), Firefox in 85 (January 2021), and Safari in 15.4 (March 2022), which is the version that completed the set across all engines. Every current browser also uses :focus-visible in its own default user-agent stylesheet, so the modern default ring you see on a tabbed-to button is already :focus-visible behavior, not plain :focus.
Because it is fully supported, you do not need a @supports guard or a polyfill. If you are still carrying focus-visible.js or a :-moz-focusring rule from an older codebase, delete both. The class-based polyfill output (.js-focus-visible .foo:focus:not(.focus-visible)) can come out at the same time.
Where it fits with the other focus selectors
:focus-visible is about which focus to indicate. Its sibling :focus-within is about where focus is: it matches an ancestor when any descendant holds focus, which is how you highlight a whole form group or keep a dropdown styled while a child link is focused. The two compose well: style a container with :focus-within and style the actual focused control with :focus-visible.
If you have moved to the modern relational selector, you can express container-level focus with the :has() parent selector as form:has(:focus-visible), which is the more general tool. :focus-within is shorter and clearer for the common case, so I still reach for it first.
FAQ
See also
- CSS :focus-within: style a parent when a child has focus: the sibling selector that matches an ancestor when any descendant is focused, for form groups and dropdowns.
- CSS accent-color: brand-style checkboxes, radios, and form controls: tint native form controls that sit alongside your focusable elements.
- The CSS :has() parent selector: the general relational selector that can express
form:has(:focus-visible)and much more.
Sources
Authoritative references this article was fact-checked against.
- :focus-visible CSS pseudo-class (MDN)developer.mozilla.org
- :focus CSS pseudo-class (MDN)developer.mozilla.org
- Understanding WCAG 2.4.7 Focus Visible (W3C)w3.org
- Understanding WCAG 2.4.11 Focus Not Obscured Minimum (W3C)w3.org





