Why your focus ring vanishes in dark mode
A ring designed on a white mockup is a coin toss once the page can be dark. The fix is not a darker ring. It is a second tone.
Here is a pattern that shows up in almost every design system that added dark mode after the fact.
The focus ring was chosen on a white artboard. It is a confident brand blue, maybe three pixels wide. It passes every contrast check anyone ran, because every check was run on white. Then dark mode ships. The surfaces go to near-black, the brand blue stays the brand blue, and the ring now sits at roughly 2:1 against the page. Keyboard users in dark mode lose their cursor, and nobody notices because nobody on the team tabs through the dark theme.
The arithmetic
Contrast is a ratio between two luminances. A mid-tone color, which is what most brand colors are, sits in the middle of the luminance range. Against white it has decent contrast. Against black it also has decent contrast. Against anything close to its own luminance, it has almost none.
That is the trap. A ring that clears 3:1 on white often clears 3:1 on near-black too, which makes it look fine in both themes. But dark mode is not one color. It is a range of grays, blues, and surfaces, and the ring will land on a card, a modal, a sidebar, and a hover state that are all different. Somewhere in that range there is a surface whose luminance is close to the ring's, and that is where it disappears.
You can watch it happen in the Focus Lab. The default ring there is this site's own blue. It passes on white and on near-black. It fails on mid gray, dark gray, navy, and bright blue, which is to say on most of what a dark theme is made of.
Why "pick a lighter ring for dark mode" is not enough
The obvious fix is a second ring color for dark mode: light blue on dark surfaces, dark blue on light surfaces. It helps, and it is better than nothing. But it moves the trap rather than removing it. Now the light ring fails on the light surfaces that dark mode still has: a white image, a yellow warning banner, a code block with a pale background, a selected row.
Every single-tone ring has a set of backgrounds it cannot be seen on. Changing the tone changes the set. It does not shrink it.
The fix is two tones
Put a light band and a dark band next to each other. Whatever the ring lands on, one of the two bands contrasts with it, and the other band contrasts with the first. The ring always has a visible edge.
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
outline: none;
border-radius: 8px;
box-shadow: 0 0 0 3px #ffffff, 0 0 0 6px #0b3d5c;
}
This single rule works in light mode, in dark mode, on a photo, and on a striped table, with no media query. In the lab it scores twelve of twelve.
If your design system already has a --ring token, the smallest change is to add a --ring-contrast token next to it and use both. Resist the urge to make the second tone semi-transparent to "blend in." Transparency gives you a different color on every surface, which is the problem you are trying to escape.
A quick test you can run today
- Switch your product to dark mode.
- Put your hands on the keyboard and press Tab twenty times.
- Count how many times you lost track of where focus was.
If the answer is more than zero, your ring has a background it cannot survive. Open the lab, find the one that fails, and add the second tone.