Focus Visible Styles
Replace outline:none with :focus-visible designs that meet non-text contrast and stay out of the way of mouse users.
- accessibility
- focus-visible
Keyboard users need to see where focus is. outline: none without a replacement fails WCAG 2.4.7 Focus Visible. Modern CSS :focus-visible lets you show strong rings for keyboard focus while avoiding “stuck” rings on every mouse click for many controls.
Docs: MDN :focus-visible, WCAG 2.4.7, 2.4.13 Focus Appearance (2.2).
The anti-pattern
/* Don’t */
*:focus {
outline: none;
}
Baseline replacement
:focus {
outline: none; /* only if you replace below */
}
:focus-visible {
outline: 2px solid var(--focus, #005fcc);
outline-offset: 2px;
}
Or keep native outline for :focus-visible only:
:focus:not(:focus-visible) {
outline: none;
}
Design requirements
| Property | Guidance |
|---|---|
| Contrast | Focus indicator ≥ 3:1 against adjacent colors (non-text) |
| Thickness | Clearly visible; WCAG 2.2 Focus Appearance adds metrics for AAA/AA targets — aim for obvious |
| Offset | Helps on busy backgrounds |
| Shape | Don’t rely on color alone — thickness/offset help |
button:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 3px;
box-shadow: 0 0 0 1px #fff; /* separates from similar bg */
}
Components that need care
- Dark buttons on dark backgrounds — ring must still contrast
- Inputs with thick borders — offset rings
- Cards that are links — focus on the whole hit target
- Custom checkboxes — style the visible control, not only the native input if it’s visually replaced
Never remove focus for keyboard
Some designs show focus only on :focus-visible — good. Removing all focus indicators for “clean UI” is a product bug.
Testing
- Tab through the page — every interactive control shows a ring.
- Mouse click buttons — acceptable if ring hidden via
:focus-visiblebehavior. - High contrast / forced colors — check
forced-colorsmedia if you support Windows HCM.
@media (forced-colors: active) {
:focus-visible {
outline: 3px solid CanvasText;
}
}
Footguns
outline: noneglobally in a reset.- Focus ring with <3:1 contrast.
overflow: hiddenclipping rings — use offset/shadow.- Focusing elements that aren’t meant to be in tab order.
Interview out-loud answer
“I never remove focus styles without a visible replacement. I use :focus-visible with a high-contrast ring and offset, verify 3:1 non-text contrast, and keyboard-test every interactive control.”
Polyfills and older browsers
:focus-visible is widely supported now. For legacy support, the WICG polyfill or :focus fallback rings are acceptable. Never ship a global outline killer for “IE consistency” on modern stacks.
Extra practice
Write a minimal demo in a scratch file or the playground: one happy path, one failure path, and one boundary input. If you cannot exhibit a bug that the pattern prevents, you do not own the concept yet — re-read the primary docs linked below and tighten the example until the failure is obvious.
Notes from real codebases
Teams that succeed here keep the rules mechanical: lint where possible, CI for the rest, and a short human checklist for what automation cannot see. Document exceptions with an owner name and a removal date so “temporary” escapes do not become permanent architecture.
Related on this site
- Focus management
- Keyboard accessibility checklist
- Color contrast requirements
- Skip links
- WCAG principles POUR
Further reading
Related guides
- Accessible Combobox PatternBuild or evaluate comboboxes with APG keyboard behavior, aria-expanded/activedescendant, and filterable listbox pairing.
- Accessible Forms ErrorsLabel inputs, associate errors with fields, announce failures, and avoid placeholder-only forms that break accessibility.
- Accessible Menus PatternAPG menu and menubar keyboard model — arrow navigation, escape to close, aria-expanded, and when a disclosure is enough.
- Accessible Modals PatternsModal dialogs that work: focus trap, Escape, return focus, aria-modal, and why native dialog or APG patterns beat div soup.
- Accessible Names ComputationHow browsers compute accessible names from content, labels, aria-label, and labelledby — and how that powers Testing Library queries.