When to use
Every interactive element on every page: links, buttons, inputs, custom widgets. Keyboard users need to see where they are. The usual failure is a CSS reset or a design request ("remove the blue ring") that sets outline: none with nothing in its place.
A global focus style
:root {
--focus: #0b57d0;
}
@media (prefers-color-scheme: dark) {
:root {
--focus: #8ab4f8;
}
}
:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 2px;
}
html {
scroll-padding-top: 5rem;
}:focus-visibleshows the ring for keyboard focus and hides it for mouse clicks on buttons. Text inputs show it on click too, which is correct.outlinefollows border radius in current browsers, does not change layout, and stays visible in Windows forced colors mode.box-shadowrings disappear in forced colors; if you use one, addoutline: 2px solid transparentas well so forced colors draws it.outline-offsetkeeps the ring off the element's own border, so it contrasts with the page background instead of the button fill.scroll-padding-topequal to a sticky header's height stops focused elements from scrolling under the header.
Contrast of the indicator
The ring must stand out against what is next to it: aim for 3:1 against the background it sits on. One brand color rarely works on both light and dark sections. For mixed backgrounds use a two-tone ring:
:focus-visible {
outline: 3px solid #ffffff;
box-shadow: 0 0 0 6px #1a1a1a;
}Not hidden by sticky or fixed content
A focused element must not be fully covered by a sticky header, a cookie banner, a chat button or a bottom bar. Fixes: scroll-padding-top and scroll-padding-bottom on html sized to the sticky elements; make cookie banners modal or move them out of the way; never let a fixed footer cover the last form field.
DO
Put the focus style in your base stylesheet, before components, so every element has it by default.
Keep custom widgets focusable where they should be (
tabindex="0"on a widget root, rovingtabindexinside composites) so they get the same ring.For components with dark fills, set a per-component focus color that contrasts with the surrounding background.
Check that the ring is not clipped by
overflow: hiddenon a parent; use a negativeoutline-offsetinside clipped containers (cards, carousels).
DO NOT
Do not write
*:focus { outline: none },button:focus { outline: 0 }or Tailwindoutline-none/focus:outline-nonewithout afocus-visible:replacement.Do not signal focus only by a color change of text or background with low contrast; a 1 px color shift is not visible.
Do not use
:focus-withinas the only focus style for a composite; the focused child still needs its own.Do not remove the ring because "it looks odd on click";
:focus-visiblealready solves that.
React and frameworks
Component libraries and CSS resets (Tailwind preflight, some normalize variants) keep or remove outlines differently; check one button, one link and one input after adding them. In Tailwind, use focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-blue-700, not focus:ring alone. In CSS-in-JS, put the focus rule in the global style, not per component, so nothing ships without it.
What to verify
Tab through the whole page: every stop shows a ring you can see from a normal viewing distance, in light and dark mode.
Clicking a button with the mouse does not leave a ring; clicking into a text field does.
Tab to elements just below a sticky header and just above a fixed footer: each one is fully in view.
In Windows high contrast or forced colors emulation, the ring is still drawn.