Skip to story

Accessibility

:focus-visible vs outline: none

9 min read · May 31, 2026 ★ Flagship

Reading level

The invisible navigation disaster

You notice that default focus rings look a bit ugly on your buttons. You add outline: none to fix it. The blue rings disappear. Design is happy. But users who navigate with keyboards — and there are more than you think — can no longer see where they are on the page. Every button, link, and form field is now invisible to tab navigation.

outline: none on :focus is one of the most damaging accessibility mistakes in web development. It removes the only visual cue keyboard users have for their current position. The fix: use :focus-visible, which shows focus rings only when the user is navigating with a keyboard — not when they click with a mouse.

The WCAG 2.2 Success Criterion 2.4.11 (Focus Appearance, AA) and 2.4.12 (Focus Appearance, AAA) codify the minimum size and contrast requirements for focus indicators. outline: none fails 2.4.7 (Focus Visible, AA). The :focus-visible pseudo-class, now baseline in all browsers, is the correct solution — it's how the browser already differentiates mouse from keyboard interaction internally.

How :focus-visible works

Browsers know whether you're navigating with a keyboard or a mouse. :focus-visible only applies when the browser thinks a visible focus indicator would help. If you clicked a button with a mouse, :focus-visible doesn't apply — no ring. If you tabbed to a button with the keyboard, it does apply — ring shows.

/* ✅ Remove focus ring on mouse click, keep it for keyboard */
button:focus:not(:focus-visible) {
  outline: none; /* hides on mouse click */
}
button:focus-visible {
  outline: 2px solid #4E7A68; /* shows on keyboard nav */
  outline-offset: 3px;
}

The browser's heuristic for :focus-visible (from the spec): matches if any of these are true — the element received focus via keyboard, the element is a text input, the element type usually benefits from a visible focus indicator. The heuristic is reliable across Chrome, Firefox, and Safari.

A simpler, modern approach leverages the fact that browsers style :focus-visible correctly by default — just don't override :focus for mouse clicks:

/* ✅ Modern: customize focus-visible, leave focus alone */
:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 4px;
  border-radius: 4px;
}

The :focus-visible heuristic uses a "has the user interacted with the keyboard recently" internal flag. This means typing in an input then clicking a button can briefly show :focus-visible on the clicked element. This is intentional — the assumption is that a keyboard user is more likely to benefit from the ring even in this edge case. Design systems should account for this in component specs.

The global CSS reset that blinded keyboard users

A designer was reviewing the staging site and noticed the blue focus rings around buttons when clicking them. "These look out of place," she said. "Can we remove them?" A developer added * { outline: none } to the global stylesheet. The rings disappeared. Design was satisfied. The change shipped.

Three weeks later, an accessibility audit flagged the site as failing WCAG 2.4.7 — Focus Visible. Keyboard-only users navigating with Tab had zero visual indication of their position. Every button, link, and form field was invisible in keyboard navigation. Users who relied on keyboard — people with motor disabilities, power users, anyone not using a mouse — were navigating blind.

The root cause was a category error: the designer saw a visible focus ring on a mouse-clicked button and assumed it was unnecessary. It was — for mouse users. But :focus matches every focus event regardless of input method. Removing outline from :focus with a global rule removed it from keyboard navigation too. There was no way to distinguish mouse focus from keyboard focus in the original CSS — the selector :focus doesn't know how the element received focus. The :focus-visible pseudo-class exists specifically to provide that distinction, but wasn't used here.

The accessibility audit result was a direct WCAG 2.4.7 (Focus Visible, AA) failure. This criterion requires that any keyboard-operable UI component can be visually focused. The global outline: none reset is so common it has its own entry in every accessibility checker's rule set — axe-core rule scrollable-region-focusable and the broader focus-visible rule both catch it. The failure had been in production for three weeks across a surface with 12,000 monthly active keyboard users, a number the team discovered only after pulling keyboard-nav session data they had never looked at before.

:focus-visible — the ring that knows when to show up

The fix removed the global * { outline: none } rule and replaced it with two targeted rules. Mouse clicks on buttons: no ring. Keyboard navigation: a clean, styled ring. The designer looked at the result and approved it — she had never wanted the ring to appear on mouse clicks; she just hadn't known that was achievable.

The keyboard users regained visibility of their position as they tabbed through the page. The accessibility audit was cleared. The change took 20 minutes to write and test. The three-week regression had cost weeks of keyboard users navigating blindly through a product they couldn't use effectively.

The implementation replaced the global reset with a targeted :focus-visible block and an explicit suppression of the ring on mouse-click focus:

/* Remove :focus ring only for mouse/pointer interactions */
*:focus:not(:focus-visible) {
  outline: none;
}

/* Styled ring for keyboard navigation only */
*:focus-visible {
  outline: 2px solid var(--focus-ring-color, #4E7A68);
  outline-offset: 3px;
  border-radius: 4px;
}

The design system also introduced a --focus-ring-color token so individual components could override the ring color for contrast on dark backgrounds without duplicating the selector logic.

The post-fix measurement included adding keyboard navigation session tracking to analytics — something the team hadn't done before. 8.3% of monthly active users were keyboard-primary navigators. The three-week regression had exposed all of them to a broken experience that was invisible in clickstream analytics (which measures clicks, not tab presses). The systemic fix included adding axe-core to the CI pipeline targeting the focus-visible rule, and adding a keyboard navigation smoke test to the release checklist — tab through five critical flows before any deploy. The process gap was as important to address as the CSS bug.

Pattern at a glance

Annotated example: outline: none vs :focus-visible ring on keyboard nav

❌ OUTLINE: NONE — keyboard user sees nothing

No ring visible — where is the focus? Keyboard user cannot tell.

✅ :FOCUS-VISIBLE — clean ring on keyboard nav

"Home" is focused — ring is clear, styled, and accessible.

Try it: tab navigation with and without focus ring

Tab through the buttons below in "Broken" mode — you can't see where the focus is. Switch to "Fixed" and tab again — you can follow exactly where you are.

Try clicking a button with the mouse in fixed mode — no ring. Then press Tab — ring appears. That's :focus-visible correctly differentiating mouse from keyboard.

The fixed focus ring uses outline (not border or box-shadow) — outline doesn't affect layout, supports border-radius in modern browsers, and works in high-contrast mode.

⚡ Interactive demo

Implementing :focus-visible across a design system

The complete pattern: suppress the focus ring for mouse clicks, show a styled ring for keyboard navigation. Two rules cover the full case.

/* Step 1: Suppress :focus ring for mouse/pointer only */
*:focus:not(:focus-visible) {
  outline: none;
}

/* Step 2: Style the keyboard-nav ring */
*:focus-visible {
  outline: 2px solid var(--focus-ring-color, #4E7A68);
  outline-offset: 3px;
  border-radius: 4px;
}

Key pitfall: never use *:focus { outline: none } without the :not(:focus-visible) guard. The bare rule removes focus rings for keyboard users. Always pair the suppression with a :focus-visible rule that provides the visible ring.

Framework-specific pattern for React component libraries — define the focus ring as a design token and apply it through a base component class:

/* tokens.css — design system layer */
:root {
  --focus-ring-color: #4E7A68;
  --focus-ring-width: 2px;
  --focus-ring-offset: 3px;
}

/* base.css — applied to all interactive elements */
.interactive:focus:not(:focus-visible) {
  outline: none;
}
.interactive:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
  border-radius: 4px;
}

Four implementation notes:

  1. Use outline, not box-shadow or border. Outline doesn't affect layout, supports border-radius in modern browsers, and works in Windows High Contrast Mode. box-shadow and border do not render in High Contrast Mode.
  2. outline-offset creates breathing room. 2–4px offset prevents the ring from overlapping the element's own border, making it visible on both light and dark backgrounds.
  3. Dark backgrounds need a different ring color. A dark green ring on a dark background fails WCAG 1.4.11 (non-text contrast). Use a light ring color or a double ring (white outline + colored outline) via outline + box-shadow for complex backgrounds.
  4. High Contrast Mode requires outline. Windows High Contrast Mode overrides colors but preserves outline visibility. box-shadow is invisible in High Contrast Mode. Use outline as the primary focus indicator, not a box-shadow workaround.

In a design system, focus ring tokens belong in the same tier as color and spacing tokens — not as an accessibility afterthought. Define --focus-ring-color, --focus-ring-width, and --focus-ring-offset as component-level overrideable tokens so dark-background components can set a lighter ring without duplicating the selector logic. For lint enforcement, add axe-core rule focus-visible to your CI gate — it catches outline: none without a :focus-visible counterpart. The WCAG 2.2 SC 2.4.11 minimum requirement is a 3:1 contrast ratio between the focus indicator and the adjacent colors, and a minimum perimeter area equivalent to a 2px outline around the component. Design your --focus-ring-color to pass this check against both your light and dark surface tokens — verify with a contrast checker at token-definition time, not at QA time. Add a Playwright keyboard-navigation smoke test that tabs through all primary navigation elements and asserts each has a visible outline style on :focus — run it in both light and dark modes.

References

Remember

Key takeaways

  • Never use outline: none on :focus without a replacement — it makes your site invisible to keyboard users.
    :focus-visible shows the ring only when a keyboard user would benefit — it's browser-native and handles the heuristic for you.
    WCAG 2.2 SC 2.4.11 requires focus indicators with minimum 3:1 contrast ratio and 2px perimeter area — design your focus ring to meet the spec.
  • Use outline (not border or box-shadow) for focus rings — outline doesn't affect layout and works in Windows High Contrast mode.
    outline with border-radius works in Chrome, Firefox, and Safari now — you no longer need box-shadow as a border-radius workaround.
    Add :focus-visible to your design system component spec as a first-class token — --focus-ring-color, --focus-ring-width, --focus-ring-offset — not an afterthought.

Enjoyed this case?

Case 1 of 3 in Accessibility · 1 of 31 live

Keep going

Finish this takeaway, then continue the track — Casey saved your spot locally.

Sign in with email to sync progress across devices (beta).

Inside the Casebook

New cases every few weeks — patterns from production UI engineering. Double opt-in, easy unsubscribe.

No spam. Unsubscribe anytime. Emails sent via Buttondown.

RSS feed
Casey, junior (idle)
Casey · Junior

Hey! I'm Casey — scroll through the case and I'll chime in with hints.