WCAG 2.4.7 — Focus Visible
Every focusable element must have a visible focus indicator. The default browser ring is functional and ugly; the design-systems response is too often to remove it without replacing it. Removing it without a replacement is the failure.
What this requires
When any element receives keyboard focus, there must be a visible
indicator of that focus. The browser's default outline counts. A custom
ring counts. A border change counts. What does not count is removing
the indicator entirely with outline: none and not replacing it —
which is what almost every reset CSS in the wild does by default.
How AI coding tools fail this
The most damaging failure: outline: none (or focus:outline-none in
Tailwind) applied globally to "clean up" the look, with no replacement
focus style. The keyboard user gets no visual signal at all as they
tab; they have no way to know which control will receive the next
keystroke.
The second pattern: focus indicators only on hover, or only via
:focus without :focus-visible. The :focus selector also matches
mouse clicks on macOS; :focus-visible is the modern selector that
isolates keyboard focus. Without it, the focus ring flashes on every
click, which designers don't want, which is why outline: none keeps
shipping.
The third: focus styles that exist but fail 3:1 contrast against the adjacent background. A thin light-grey ring around a white button is technically a focus indicator but invisible to anyone with low vision.
Edge cases
:focus-visibleis the right primitive — it gives you the designer aesthetic (no ring on mouse click) without breaking the keyboard user. Default to it for every interactive element.- Focus ring offset (a small gap between the element and the ring) helps the ring meet the 3:1 non-text contrast threshold even on dark elements.
- Dynamic focus — moving focus programmatically after a route
change or modal close — still needs a visible indicator at the
destination. Set
tabIndex={-1}on the target and call.focus(). - Skip-link targets are often
<main>or a heading. They needtabIndex={-1}to be focusable, and a visible ring when they receive focus. - Disabled controls don't receive focus, so they don't need an indicator — but make sure they are actually disabled (not just styled to look disabled while remaining focusable).
How Jeikin handles this
Jeikin's design-rule layer flags outline-none / outline: 0 usage in
component CSS that lacks a corresponding focus-visible style. The MCP
server returns the project's design tokens for focus ring colour,
width, and offset so the AI assistant ships the team's actual focus
style instead of an ad-hoc one.