Errori e fix
Focus ring invisibile: le cause e la correzione in CSS
Un reset globale con `outline: none` cancella l'indicatore di focus senza sostituirlo. La correzione è `:focus-visible` con outline visibile e offset, più un controllo nei casi di overflow.
In sintesi
- `outline: none` senza alternativa rende la pagina inutilizzabile da tastiera.
- `:focus-visible` mostra l'indicatore solo quando serve: tastiera e lettori di schermo, non ogni click.
- Un ring con `overflow: hidden` sul contenitore viene tagliato: usa `outline` con offset o sposta l'overflow.
Risposta breve: un reset globale con outline: none cancella l'indicatore di focus. Sostituiscilo con :focus-visible { outline: 2px solid; outline-offset: 2px }: la pagina torna usabile da tastiera e il mouse non vede cambiamenti.
Come il browser disegna l'indicatore
outline viene dipinto fuori dal box del bordo, in un passaggio separato, e non partecipa al layout: aggiungerlo o toglierlo non sposta nulla. box-shadow è un'ombra dell'elemento, e il contenitore con overflow: hidden la taglia insieme al contenuto. In forced-colors la UA rimappa i colori di sistema: l'outline diventa Highlight e resta visibile, uno sfondo o un'ombra personalizzati vengono sostituiti.
:focus-visible non è una media query sulla tastiera, è un'euristica della UA. In generale l'indicatore appare quando l'ultima interazione era da tastiera, e appare sempre sui campi di testo e sugli elementi che ricevono il focus da script dopo un comando da tastiera. È una scelta del browser, non una garanzia dello standard: per questo va verificata, non dedotta.
Le tre cause
| Causa | Come la riconosci | Correzione |
|---|---|---|
| reset globale | nessun outline su nessun elemento | sostituisci con :focus-visible |
| ring tagliato | il bordo appare solo su tre lati | usa outline con offset |
| colore a basso contrasto | il focus c'è ma non si vede | almeno 3:1 rispetto allo sfondo |
La correzione CSS
: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 è preferibile a box-shadow perché non occupa spazio e non viene tagliato da overflow: hidden. L'offset stacca il bordo dal contenuto e lo rende visibile anche su sfondi colorati.
La variante Tailwind
Salva
focus-visible: è la variante corretta: focus: si attiva anche al click e rende il feedback rumoroso. Se nel progetto esiste già un token di focus, usa quello invece di un colore fisso.
Il passo diagnostico
Il controllo utile non è leggere il foglio di stile, è elencare i controlli che restano senza indicatore. Incolla questo blocco nella console e premi Tab nella pagina per verificare i casi trovati.
[...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 : ""));
L'elenco contiene i candidati, non la diagnosi: un elemento può avere l'indicatore sul wrapper con :focus-within. Se invece l'elenco è vuoto e il ring non si vede, il problema è il contrasto o il clipping, non la regola.
Quando la correzione non funziona
Se il focus non si vede su un bottone dentro un contenitore con overflow: hidden, il problema è il clipping: passa a outline o sposta l'overflow. In modalità a contrasto elevato il browser sostituisce i colori: un box-shadow sparisce, un outline no. Se il focus non arriva proprio, cerca tabindex="-1" o un pointer-events: none sul controllo.
Quando la correzione comune peggiora le cose
Per far stare il ring dentro una card con overflow: hidden la correzione tipica è togliere l'overflow al contenitore. Il bordo torna visibile e il layout perde il suo contratto: la tabella larga esce dalla pagina, il carosello mostra la slide successiva, lo scorrimento orizzontale compare sul body. Su mobile il danno supera il difetto.
L'altra scorciatoia è sostituire l'indicatore con un cambio di background-color o di border-color. Funziona a schermo normale e sparisce in forced-colors, dove la UA forza sfondo e colori di sistema: chi usa il contrasto elevato perde l'unico segnale. Se devi cambiare qualcosa, cambia outline-color, che viene rimappato.
Un limite da mettere nel conto
L'indicatore appartiene al singolo elemento: non puoi disegnare un ring unico attorno a un widget composito come una listbox o una toolbar. Il massimo che ottieni è lo stile sul contenitore con :focus-within, che però non dice quale voce è attiva. Per quello serve un indicatore interno, e nessuna proprietà CSS lo genera da sola.
Come prevenirlo
- Vieta
outline: nonesenza una regola di sostituzione nello stesso file. - Aggiungi un test da tastiera: Tab attraverso la pagina, screenshot a ogni passaggio.
- Verifica il contrasto del ring con lo stesso metodo usato per il testo.
Dove continuare
Nel catalogo: form di login, componenti e blocchi. Le guide: checklist accessibilità e palette con contrasto misurato; il confronto: Radix, Base UI e React Aria.
Domande frequenti
`focus:` e `focus-visible:` sono equivalenti?
No. `focus:` si attiva anche al click del mouse; `focus-visible:` solo quando il browser ritiene che il focus sia da tastiera.
Perché il ring non si vede su un bottone con overflow?
Il ring viene disegnato fuori dal box e il contenitore con `overflow: hidden` lo taglia. Passa a `outline` con `outline-offset` o cambia il contenitore.
Quale contrasto serve?
WCAG 2.2 chiede un indicatore con contrasto 3:1 rispetto ai colori adiacenti e area minima. Un ring da 1px spesso non basta.
Come funziona in forced-colors mode?
Il browser sostituisce i colori. Se rimuovi l'outline, in modalità a contrasto elevato non resta nulla: usa `outline`, non `box-shadow`.
Devo testarlo su ogni componente?
Almeno su link, bottoni, input, select, menu e dialog. Sono i punti in cui il focus si perde più spesso.