CSS Focus State Generator
Build a focus ring on :focus-visible, previewed on real controls you can tab to, with the 3:1 contrast WCAG asks for measured against both neighbours.
Enable JavaScript to change the ring; the CSS below is for the defaults.
Two pixels is the practical minimum, and WCAG 2.4.13 in 2.2 asks for an area at least as large as a 2px perimeter.
Space between the control and the ring. A positive offset is what stops a ring disappearing into a dark button.
Used to measure the ring's contrast. WCAG 1.4.11 asks for 3:1 against adjacent colours.
Press Tab to move through these. Clicking them should leave no ring; tabbing to them should.
/* :focus-visible, so a mouse click leaves no ring and a Tab press does. */
.button:focus-visible {
outline: 3px solid rgb(30 86 236);
outline-offset: 2px;
box-shadow: 0 0 0 5px rgb(255 255 255);
}
/* For browsers without :focus-visible, keep the ring on :focus and remove
it only where :focus-visible is understood. */
.button:focus {
outline: 3px solid rgb(30 86 236);
outline-offset: 2px;
}
.button:focus:not(:focus-visible) {
outline: none;
box-shadow: none;
}
/* The ring is 5.84:1 against the background and
3.24:1 against the component. WCAG 1.4.11 asks for 3:1
against both, which this clears. */
A focus ring is the only thing telling a keyboard user where they are, and it is the style most often
deleted for looking untidy. outline: none with nothing after it is the most common accessibility failure
in a modern codebase, and it is invisible to anybody testing with a mouse.
Three rules cover nearly all of it.
Use :focus-visible, not :focus, so a mouse click on a button leaves no ring and a Tab press does. Give
the ring an offset, because a ring that sits flush against a dark button disappears into it. And check the
contrast: WCAG 1.4.11 asks for 3:1 against adjacent colours, which means the ring has to be visible
against both the background and the control itself. This tool measures both and prints the numbers.
The preview is real controls. Press Tab to walk through them, because a ring painted on a div proves nothing about when the browser decides to show one.
How to use
- Set the ring colour, width and offset. Two pixels is the practical minimum width.
- Put in the background behind the control and the control’s own colour, so the contrast can be measured against both.
- Keep the second ring on if this style is used on both light and dark surfaces. Keep the legacy rules on
unless you have dropped support for browsers without
:focus-visible.
Example
/* :focus-visible, so a mouse click leaves no ring and a Tab press does. */
.button:focus-visible {
outline: 3px solid rgb(30 86 236);
outline-offset: 2px;
box-shadow: 0 0 0 5px rgb(255 255 255);
}
/* For browsers without :focus-visible, keep the ring on :focus and remove
it only where :focus-visible is understood. */
.button:focus {
outline: 3px solid rgb(30 86 236);
outline-offset: 2px;
}
.button:focus:not(:focus-visible) {
outline: none;
box-shadow: none;
}
/* The ring is 5.84:1 against the background and
3.24:1 against the component. WCAG 1.4.11 asks for 3:1
against both, which this clears. */
The :focus:not(:focus-visible) rule is the whole trick for older browsers: a browser that does not
understand :focus-visible cannot match that selector, so it keeps the ring. One that does understand it
removes the ring only for mouse clicks.
Pitfalls
Never outline: none on its own. If you remove the default ring you owe the page a replacement. Removing
it and stopping there breaks keyboard use entirely.
:focus fires for mouse clicks too. That is why buttons used to keep a ring after being clicked, and why
people started deleting rings. :focus-visible is the fix.
A flush ring disappears. On a dark button, a dark ring with no offset is invisible. outline-offset: 2px
is usually all it takes.
Contrast has two sides. The ring must be visible against the page and against the control. A blue ring on a blue button fails one of them, and the tool says which.
One colour does not survive both themes. A two-colour ring, dark with light just outside it, is how a single style works on a white card and a dark header.
The outline follows the border radius. In every current browser, so a rounded button gets a rounded ring
for free. The box-shadow fallback used before that is only needed for very old versions.
Do not forget custom controls. A div with tabindex="0" acting as a button needs the same treatment, and
it gets nothing by default.
Compatibility
:focus-visible works in Chrome and Edge 86, Firefox 85 and Safari 15.4. The optional legacy block covers
anything older and is safe to ship: a browser that does not understand :focus-visible simply never
matches the removal rule.
outline-offset works everywhere in use, and modern browsers follow border-radius with the outline, which
they did not always do. Where a very old browser has to show a rounded ring, the box-shadow version in the
output does that job as well.
The rules and the contrast arithmetic live in one module, mirrored in PHP for the first paint, with a case file comparing them on every build, including a case where the ring, the background and the component are all the same colour.
The contrast figures use the same WCAG relative-luminance formula as the contrast checker, and the verdict requires both ratios to clear 3:1 rather than only one, which is what 1.4.11 asks for.
Frequently asked questions
What is the difference between :focus and :focus-visible?
:focus matches whenever an element has focus, including after a mouse click. :focus-visible matches only
when the browser thinks a visible indicator is useful, which is keyboard navigation and text inputs.How thick should a focus ring be?
Why does my ring vanish on dark buttons?
outline-offset and check the ratio against the
control’s own colour, not just the page background.Should I use box-shadow instead of outline?
outline is better: it does not affect layout, it follows the border radius, and it survives High Contrast
mode in Windows, where box shadows are dropped. Use box-shadow for a second ring, not the first.Do I still need the legacy rules?
:focus-visible rule.