Visible focus on custom controls
If a keyboard user can't see where focus is, the rest doesn't matter.
A focus indicator is the small visible ring or outline that says "you are here." For mouse users, it's mostly invisible. For keyboard users, it's the only way to know which element will react to Enter, Space, or arrow keys.
Browsers ship default focus styles. They aren't pretty. The most common accessibility regression is a designer wiping them out with outline: none and not putting anything back. This lesson is about putting them back well.
Rule one: don't remove focus styles
The CSS line that has caused more accessibility bugs than any other:
*:focus { outline: none; }This silently breaks the keyboard experience for every user, on every control, forever. There is one — exactly one — situation in which removing the default outline is acceptable: when you immediately replace it with a custom focus style that's at least as visible.
WCAG 2.4.7 ("Focus Visible") makes this a requirement, not a suggestion. Every focusable element must have a visible focus indicator.
If you find yourself writing outline: none without an immediate outline: ... elsewhere, stop. You're shipping a bug.
Why outline beats box-shadow alone
A common modern pattern uses box-shadow for focus rings, often colored to match the brand:
button:focus {
outline: none;
box-shadow: 0 0 0 3px #3b82f6;
}This looks great in normal browsing. But Windows High Contrast Mode (now called Forced Colors Mode) and several other accessibility modes throw away most colors and all box-shadows. They keep outline. So in Forced Colors Mode, this button has no focus indicator at all.
The fix is small: keep the outline, and let box-shadow be a decorative addition.
button:focus-visible {
outline: 2px solid currentColor; /* survives Forced Colors */
outline-offset: 2px; /* gap between control and ring */
box-shadow: 0 0 0 4px var(--accent); /* prettier in normal mode */
}A few details worth knowing:
currentColormakes the outline match the text, which Forced Colors Mode also recolors. It always stays visible.outline-offsetputs a small gap between the element and the ring, which makes it more readable on dense layouts.outlinedoesn't take part in the box model — it doesn't push siblings around when it appears. That's why it's perfect for focus rings.
Test focus visibility in Windows Forced Colors Mode (or use the "Forced Colors" emulation in Chrome/Edge DevTools). If your ring vanishes, switch from box-shadow to outline.
Mouse vs. keyboard with :focus-visible
The default :focus pseudo-class matches whenever an element is focused — by tab, click, or .focus() call. That sometimes makes designs feel noisy: a button shows a focus ring after you click it, even though you used a mouse and don't need it.
:focus-visible is a refinement: it matches only when the browser believes focus came from a keyboard (or another non-pointer input). Click a button and you get focus, but no ring. Tab to it and you do.
button:focus { outline: none; } /* hide default */
button:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}That's the modern recipe: hide the default :focus, paint your custom ring on :focus-visible. Mouse users get a calm UI. Keyboard users get a clear ring. Everyone wins.
Don't just hide :focus with no :focus-visible rule afterward. That brings you right back to the bug we started with.
Designing a good ring
A useful focus ring is:
- High contrast. WCAG 1.4.11 ("Non-Text Contrast") asks for at least 3:1 contrast against adjacent colors. A pale gray ring on a white background fails.
- At least 2px thick. Hairline rings are easy to miss, especially on small controls or high-resolution displays.
- Distinct from hover. If your hover and focus styles look the same, a keyboard user can't tell that focus moved into the element they hovered over earlier.
- Adjacent, not inside. Use
outline-offsetto give the ring a tiny gap. A ring drawn over the element's edges sometimes blends in with borders. - Survives Forced Colors. Use
outline, notbox-shadowalone. UsecurrentColorfor the ring color.
Try it yourself
*:focus { outline: none; } as a global rule?outline survive Windows Forced Colors Mode while box-shadow does not?:focus and :focus-visible?