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.

Style

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.

A link

focus.css
/* :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

  1. Set the ring colour, width and offset. Two pixels is the practical minimum width.
  2. Put in the background behind the control and the control’s own colour, so the contrast can be measured against both.
  3. 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?
At least 2px, 3px is comfortable. WCAG 2.2 adds a criterion about the focus indicator’s area being at least as large as a 2px perimeter around the control, which is a simpler way of saying do not make it hairline.
Why does my ring vanish on dark buttons?
No offset, or not enough contrast against the button. Add 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?
Only if you support Safari before 15.4 or a browser older than about 2021. If you do not, remove them and keep the single :focus-visible rule.
Weekly drops

New tools, when there are new tools

One email when something worth using ships. No schedule to fill, so no filler.

Your address goes nowhere else, and one click unsubscribes.