Fixing Missing Focus Indicators in Custom React Components for WCAG 2.4.7 Compliance
Custom-styled buttons and interactive components look perfect in every design review, but keyboard-only users can't tell where focus is. It's rarely intentional — here's the real cause and the definitive fix for WCAG 2.4.7.
The Problem
A custom-styled button, card, or interactive <div> component looks perfect in every design review, but keyboard-only users report they can't tell where focus is on the page — tabbing through the interface leaves no visible indicator at all, or the indicator disappears the moment a component gets custom styling. This fails WCAG 2.4.7 (Focus Visible), and in practice it's usually not a deliberate choice — it's a side effect of a very common CSS reset.
Why It Happens
The outline: none reset
Somewhere in a global stylesheet or a component library's base styles, outline: none (or outline: 0) got applied to all interactive elements to remove the browser's default focus ring, usually because a designer found it visually "ugly" or inconsistent across browsers. The problem: nothing replaced it. The element is still focusable and receives keyboard focus, it's just invisible.
Custom components that aren't natively focusable
A <div onClick={...}> styled to look like a button isn't focusable at all by default — it has no tabindex, so keyboard users can't reach it whether or not you've dealt with the outline. This is a more severe version of the same underlying issue: the component was built to look interactive without being built to behave as one.
Focus rings that only work for mouse users too
Some components add a focus style using :hover or :active instead of :focus, which shows a highlight for mouse users pressing the element but never for someone tabbing to it with a keyboard — passing a casual visual check while still failing the actual requirement.
The Fix
1. Never remove the outline without replacing it
/* Bad */
button:focus { outline: none; }
/* Good */
button:focus-visible {
outline: 2px solid var(--focus-ring-color);
outline-offset: 2px;
}
:focus-visible (not plain :focus) is the key detail here — it lets the browser show the ring for keyboard navigation while suppressing it for a mouse click, which is exactly the behavior most "we removed it because it looked bad on click" complaints were actually asking for.
2. Make custom interactive elements natively focusable
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") handleClick(); }}
>
Custom Button
</div>
Better yet, render an actual <button> and style it to look however the design calls for — you get focusability, keyboard activation (Enter/Space), and correct semantics for free, instead of re-implementing all three by hand on a div.
3. Make sure the focus ring is visible against every background it appears on
A focus ring using the brand's primary color can vanish against a similarly colored background or image. Test the ring specifically on the darkest and lightest surfaces in the design system, and prefer an outline with enough contrast (and an outline-offset) to stay visible regardless of what's behind the element.
4. Audit with an actual keyboard, not just a linter
Automated accessibility linters catch missing alt text and ARIA attributes reliably, but a disappearing or invisible focus ring is best caught by literally tabbing through the page yourself with the mouse unplugged (or ignored). It's a five-minute check per page that automated tools routinely miss.
Why This Works
WCAG 2.4.7 doesn't ask for the ugly default browser ring back — it asks that keyboard users always have a visible indicator of where they are. :focus-visible solves the "it looked bad on mouse click" complaint that caused the outline to be removed in the first place, native focusable elements get the behavior right without extra code, and testing with a keyboard catches what visual QA alone won't.
Conclusion
Missing focus indicators almost always trace back to one outline: none reset with nothing to replace it, plus custom components built to look interactive without being built to behave as one. Fix both with :focus-visible and real focusable elements (or the correct role/tabindex/keyboard handling), and keyboard navigation becomes usable again without giving up any of the custom visual design.