Keyboard and Focus · 6 / 7
lesson 6

Visible focus on custom controls

If a keyboard user can't see where focus is, the rest doesn't matter.

~ 13 min read·lesson 6 of 7
0 / 7

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:

don't.css
*: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.

Watch out

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:

popular-but-incomplete.css
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.

better.css
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:

  • currentColor makes the outline match the text, which Forced Colors Mode also recolors. It always stays visible.
  • outline-offset puts a small gap between the element and the ring, which makes it more readable on dense layouts.
  • outline doesn'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.
Tip

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.

focus-visible.css
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.

Watch out

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-offset to give the ring a tiny gap. A ring drawn over the element's edges sometimes blends in with borders.
  • Survives Forced Colors. Use outline, not box-shadow alone. Use currentColor for the ring color.
Save low contrastSave shadow only — fails Forced ColorsSave outline + shadow
Three rings: pale and thin (poor), brand-matched but no outline (vanishes in Forced Colors), and outline plus optional shadow (best).

Try it yourself

check your understanding
What's wrong with *:focus { outline: none; } as a global rule?
check your understanding
Why does outline survive Windows Forced Colors Mode while box-shadow does not?
check your understanding
What's the difference between :focus and :focus-visible?
check your understanding
What contrast ratio does WCAG ask for between a focus indicator and its adjacent background?
← prevnext lesson →
KeepLearningcertificate
for completing
Keyboard and Focus
0 of 7 read