Errors and fixes
Invisible focus ring: the causes and the CSS fix
A global reset with `outline: none` removes the focus indicator and puts nothing back. Use `:focus-visible` with an outline and offset, and check overflow.
Key points
- `outline: none` with no replacement makes the page unusable from the keyboard.
- `:focus-visible` shows the indicator only when it is needed: keyboard and screen readers, not every click.
- A ring clipped by `overflow: hidden` on the container disappears: use `outline` with an offset, or move the overflow.
Short answer: a global reset with outline: none erases the focus indicator. Replace it with :focus-visible { outline: 2px solid; outline-offset: 2px }: the page works from the keyboard again and mouse users see no change.
How the browser paints the indicator
outline is painted outside the border box, in a separate pass, and takes no part in layout: adding or removing it moves nothing. box-shadow is a shadow of the element, and a container with overflow: hidden clips it along with the content. In forced-colors the user agent remaps system colours: the outline becomes Highlight and stays visible, while a custom background or shadow is replaced.
:focus-visible is not a media query for keyboards, it is a user agent heuristic. In general the indicator appears when the last interaction came from the keyboard, and it always appears on text fields and on elements that receive focus from a script after a keyboard command. That is a browser decision, not a guarantee of the standard, so check it rather than infer it.
The three causes
| Cause | How to spot it | Fix |
|---|---|---|
| global reset | no outline on any element | replace it with :focus-visible |
| clipped ring | the border shows on three sides only | use outline with an offset |
| low contrast colour | the focus is there but you cannot see it | at least 3:1 against the background |
The CSS fix
:focus-visible {
outline: 2px solid var(--ui-focus, #2563eb);
outline-offset: 2px;
border-radius: 4px;
}
/* il mouse non deve vedere il ring */
:focus:not(:focus-visible) {
outline: none;
}
outline beats box-shadow because it takes no space and survives overflow: hidden. The offset lifts the border off the content and keeps it visible on coloured backgrounds.
The Tailwind variant
Salva
focus-visible: is the right variant. focus: also fires on click and makes the feedback noisy. If the project already has a focus token, use it instead of a hard-coded colour.
The diagnostic step
The useful check is not reading the stylesheet, it is listing the controls that end up with no indicator. Paste this block into the console and press Tab through the page to confirm the elements it finds.
[...document.querySelectorAll("a[href],button,input,select,textarea,[tabindex]")]
.filter((el) => {
el.focus();
const s = getComputedStyle(el);
return s.outlineStyle === "none" || parseFloat(s.outlineWidth) === 0;
})
.map((el) => el.tagName.toLowerCase() + (el.className ? "." + el.className : ""));
The list holds candidates, not a diagnosis: an element may have its indicator on a wrapper through :focus-within. If the list is empty and the ring still does not show, the cause is contrast or clipping, not the rule.
When the fix does not work
If the focus is invisible on a button inside a container with overflow: hidden, clipping is the cause: switch to outline or move the overflow. In high contrast mode the browser replaces colours, so a box-shadow disappears and an outline does not. If focus never arrives at all, look for tabindex="-1" or pointer-events: none on the control.
When the common fix makes things worse
To keep the ring inside a card with overflow: hidden the usual move is to remove the overflow from the container. The border comes back and the layout loses its contract: the wide table spills out of the page, the carousel shows the next slide, horizontal scrolling appears on the body. On mobile the damage is larger than the defect.
The other shortcut swaps the indicator for a change of background-color or border-color. It works on a normal screen and disappears in forced-colors, where the user agent forces the background and system colours: anyone using high contrast loses the only signal. If you must change something, change outline-color, which gets remapped.
One limitation to budget for
The indicator belongs to the single element: you cannot draw one ring around a composite widget such as a listbox or a toolbar. The most you get is styling the container with :focus-within, which still does not say which item is active. That needs an inner indicator, and no CSS property produces one on its own.
How to prevent it
- Ban
outline: nonewithout a replacement rule in the same file. - Add a keyboard test: Tab through the page and screenshot each step.
- Check the ring contrast with the same method you use for text.
Where to continue
In the catalogue: login form, components and blocks. The guides: accessibility checklist and palette with measured contrast; the comparison: Radix, Base UI and React Aria.
Frequently asked questions
Are focus: and focus-visible: equivalent?
No. focus: also fires on a mouse click; focus-visible: fires only when the browser judges the focus came from the keyboard.
Why is the ring invisible on a button inside an overflow container?
The ring is drawn outside the box and the container with overflow: hidden clips it. Switch to outline with outline-offset, or change the container.
What contrast does it need?
WCAG 2.2 asks for 3:1 against adjacent colours and a minimum area. A 1px ring is often not enough.
How does it behave in forced-colors mode?
The browser replaces colours. Remove the outline and high contrast mode leaves nothing, so use outline, not box-shadow.
Do I need to test every component?
At least links, buttons, inputs, selects, menus and dialogs. Those are where focus is lost most often.