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

CausaCome la riconosciCorrezione
reset globalenessun outline su nessun elementosostituisci con :focus-visible
ring tagliatoil bordo appare solo su tre latiusa outline con offset
colore a basso contrastoil focus c'è ma non si vedealmeno 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: none senza 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.