Focus Rings Are Not Optional: A Practical Guide to Visible Focus in 2026
Removing the focus ring is the most common accessibility regression we see in audits. Here's how to keep keyboard users happy without making your designer cry.

Every accessibility audit we run starts the same way: tab through the page. Within thirty seconds we usually find a button, a custom dropdown, or an entire navigation that swallows the focus ring whole. It's the single most common regression in modern web UI, and in 2026 — with WCAG 2.2 enforcing a minimum focus appearance — it's no longer something you can punt to next quarter.
This is a practical walkthrough of how we think about focus indicators across design tokens, Figma, and the actual CSS that ships. No moralising, just patterns that work.
Why Focus Rings Keep Getting Killed
The blue Chrome ring is ugly. Designers know it, PMs know it, and the first line of CSS many teams write is some flavour of outline: none. The intent isn't malicious — it's that the default browser ring clashes with rounded corners, brand colours, and any component that already has a hover state.
The fix that gets shipped is usually one of three bad options:
- Remove the outline and forget to replace it.
- Replace it with a 1px border that disappears on busy backgrounds.
- Rely on
:hoverstyles and assume keyboard users will figure it out.
All three fail WCAG 2.4.7 (Focus Visible) and most fail the newer 2.4.11 (Focus Not Obscured) and 2.4.13 (Focus Appearance) success criteria. The last one is the kicker: it specifies a minimum area and contrast for the focus indicator itself.
What WCAG 2.4.13 actually asks for
The short version: your focus indicator needs to be at least as large as a 2 CSS pixel perimeter around the component, and it needs a contrast ratio of at least 3:1 against the adjacent colours — both the focused and unfocused states. That last clause is what trips most teams up. A subtle 1px brand-blue ring that looks gorgeous on white will fail the moment the button sits on a coloured card.
The Token Model We Use
Focus is a state, not a colour. Treat it like one in your design tokens. Here's the structure we recommend for any design system that needs to survive light mode, dark mode, and brand themes:
{
"focus": {
"ring": {
"color": {
"default": "{color.accent.500}",
"onAccent": "{color.neutral.0}",
"onDanger": "{color.neutral.0}"
},
"width": "2px",
"offset": "2px",
"style": "solid"
}
}
}
Three things to notice. First, the colour has variants based on what the focused element sits on — you can't use accent-blue on an accent-blue button. Second, the width and offset are tokens, not magic numbers, so a brand refresh doesn't require touching every component. Third, there's no focus.ring.color.brand or focus.ring.color.subtle — focus has one job, and naming it semantically prevents a junior dev from picking the wrong one because it looked nicer.
Mapping tokens to Tailwind
If you're on Tailwind 4, the cleanest setup is to expose these as CSS variables and reference them in your theme config:
@theme {
--color-focus-ring: oklch(0.7 0.18 250);
--color-focus-ring-on-accent: oklch(1 0 0);
--ring-width-focus: 2px;
--ring-offset-focus: 2px;
}
Then in your component classes:
<button class="bg-accent-500 text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[--color-focus-ring-on-accent]">
Save changes
</button>
Note the use of focus-visible rather than focus. This is the single most important behavioural change you can make: focus rings only appear when the user is navigating by keyboard, not when they click. Mouse users never see them. Designers stop complaining. Keyboard users stay safe. Everyone wins.
The Outline vs Box-Shadow Debate
For years, box-shadow was the recommended way to draw focus rings because outline ignored border-radius. That changed. Modern browsers respect border-radius on outlines, and outlines have one critical advantage: they don't get clipped by overflow: hidden parents.
We've lost hours debugging focus rings that vanish inside cards, modals, and scroll containers — all because someone used box-shadow and a grandparent had overflow: hidden. Outline avoids the entire category of bug.
Use outline with outline-offset as your default. Reach for box-shadow only when you need a two-tone ring (an inner light ring plus an outer dark ring for high-contrast situations), and even then, prefer stacking outline with a second box-shadow rather than going pure shadow.
.button:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
/* High-contrast fallback for busy backgrounds */
box-shadow: 0 0 0 4px var(--color-focus-ring-halo);
}
The Adjacency Problem
The hardest focus rings to design aren't on buttons sitting on white. They're on:
- Icon buttons inside a coloured toolbar
- List items inside a hover-highlighted row
- Inputs with their own border that already changes on focus
- Items inside a card that itself has a hover state
For each of these, the focus ring needs to be visible against both the resting background and any hover background. The simplest reliable pattern: a two-layer ring — an inner ring in the surface colour and an outer ring in the focus colour. The inner ring acts as a separator regardless of what's behind it.
.list-item:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 0;
box-shadow: inset 0 0 0 2px var(--color-surface);
}
This is the same trick the macOS focus ring uses, and it's stupidly effective on cards, table rows, and anything that sits inside a container with its own state.
Testing Without a Screen Reader
You don't need a full a11y audit to catch focus regressions. A five-minute manual pass catches most of them:
- Tab through the entire page from the URL bar. Every interactive element should show a ring that you can actually see from arm's length.
- Tab through with the page zoomed to 200%. Rings should not get cropped or hidden behind sticky headers.
- Switch to dark mode and repeat both passes.
- Open a modal and tab inside it. Focus should be trapped, and the first focusable element should be obvious.
- Try one component on a coloured background — a button on a brand-colour card, for example. The ring should still be visible.
For automated coverage, axe-core catches missing focus styles but won't catch insufficient contrast on the ring itself. Storybook with @storybook/addon-a11y plus a visual regression tool that captures :focus-visible states is the closest thing to a real safety net. We add a story variant for every interactive component called Focused that programmatically focuses the element so visual diffs flag any change to the ring.
A note on Windows High Contrast Mode
Forced colours mode (the modern name for Windows High Contrast) overrides your colours entirely. If you've used box-shadow for your focus ring, it disappears completely because shadows are stripped. outline survives. This is another reason to default to outline, and to add a forced-colors media query check on any component where you've gone custom:
@media (forced-colors: active) {
.button:focus-visible {
outline: 2px solid CanvasText;
}
}
What We'd Do First
If you're staring at a codebase that already strips focus rings everywhere, don't try to fix it component-by-component in one sprint. Do this in order:
- Add focus tokens to your design system, even if nothing consumes them yet. Get the naming agreed.
- Write a single global
:focus-visiblestyle as a baseline so every focusable element has something visible, even ugly. This stops the bleeding. - Audit your five highest-traffic interactive components — primary button, nav link, form input, dropdown trigger, card link — and replace their focus styles with the tokenised version.
- Add a
FocusedStorybook story per component and wire it into visual regression. - Only then go after the long tail.
Focus visibility isn't a polish task. It's the difference between a usable product and one that locks out everyone using a keyboard, switch device, or screen reader. If you want help auditing or rebuilding a design system that takes this seriously, our design and UX team does exactly this kind of work.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

Toast Notifications Are Lying to Your Users
Toasts feel modern, but they're quietly failing the people who need them most. Here's how we audit, fix, or replace them — with patterns that hold up under accessibility and conversion scrutiny.

Disabled Buttons Are a UX Bug: What to Ship Instead
Greyed-out buttons feel safe to designers and ship-ready to engineers. They're neither. Here's why disabled states quietly tank conversion, and the patterns we use to replace them without breaking validation.
Tap Targets, Thumb Zones, and the 44px Lie
The 44px tap target rule is a starting point, not a finish line. Here's how thumb reach, target spacing, and gesture conflicts actually break mobile UX — and what to measure instead.
