Errori e fix

Selettore genitore in CSS: cosa ha sostituito i workaround JavaScript

:has() è supportato dai browser moderni e permette di stilizzare un contenitore in base ai suoi figli. Restano due limiti reali: le webview datate e il costo su alberi enormi.

In sintesi

  • :has() è la risposta nativa alla domanda «come stilizzo il genitore in base al figlio».
  • Il supporto copre Chrome 105+, Safari 15.4+ e Firefox 121+: verificabile su caniuse, non da assumere.
  • Sul DOM grande, un :has() complesso costa: misura prima di usarlo su liste di migliaia di nodi.

Risposta breve: :has() è il selettore genitore nativo. Permette di stilizzare un contenitore in base a ciò che contiene, senza aggiungere classi da JavaScript: .card:has(img) { ... }.

Come il browser valuta :has()

Un selettore normale decide lo stile di un elemento guardando antenati e fratelli. :has() inverte la direzione: per sapere se .card:has(img) si applica, il motore deve guardare dentro la card. Il browser risolve con l'invalidazione: quando un nodo cambia, risale agli antenati che hanno una regola :has() e ricalcola solo quei rami. È il motivo per cui la funzionalità è arrivata tardi: per anni il costo di questa risalita è stato considerato proibitivo.

Due conseguenze pratiche. La prima: :has() non si polyfilla in modo affidabile, perché servirebbe osservare ogni nodo con MutationObserver. La seconda: più il selettore è profondo, più nodi entrano nell'insieme da invalidare. .card:has(img) guarda i figli diretti; .card:has(.grid .cell img) guarda tutto il sottoalbero.

Il passo diagnostico

Prima di discutere di supporto, verifica cosa fa il motore che hai davanti. Due righe nella console bastano.

CSS.supports("selector(:has(*))");                       // true = il motore capisce :has()
document.querySelectorAll("form:has(:invalid)").length; // quante form corrispondono

Se la prima riga dà false il problema è il browser o la webview, non il foglio di stile. Se dà true ma la regola non si vede, il selettore viene valutato e non corrisponde: ispeziona l'elemento e controlla che il figlio cercato sia un discendente, non un fratello e non un nodo dentro uno shadow root.

Cosa sostituisce

PrimaOraGuadagno
classe aggiunta da JS quando l'immagine carica:has(img)nessun listener, nessun reflow
stato del form gestito in Reactform:has(:invalid)feedback immediato senza stato
layout diverso se c'è un bannerbody:has(.banner)una regola, nessun wrapper

Esempi concreti

/* una card con immagine ha un padding diverso */
.card:has(img) {
  padding: 0;
}

/* il bottone di invio resta disabilitato finché il form non è valido */
form:has(:invalid) button[type="submit"] {
  opacity: 0.5;
  pointer-events: none;
}

/* niente margine sotto l'ultimo paragrafo */
.prose > *:last-child:has(+ *) {
  margin-bottom: 1rem;
}

L'ultimo esempio è volutamente sbagliato: :last-child:has(+ *) non può esistere, perché l'ultimo figlio non ha un fratello successivo. Serve a ricordare che :has() non cambia la logica dei selettori: la aggiunge.

Il supporto

Chrome 105+, Safari 15.4+ e Firefox 121+. Non è una funzionalità da polyfillare: i polyfill JavaScript sui selettori sono fragili e costosi. Se il pubblico include webview datate, aggiungi una classe dal JavaScript che già conosci e usa :has() come miglioramento progressivo.

Quando la correzione non funziona

Due limiti reali. Primo: :has() non si può usare dentro un pseudo-elemento come ::before. Secondo: su un albero molto grande il browser deve valutare i discendenti, e un selettore complesso su migliaia di righe può costare. Misura con il pannello Performance prima di applicarlo a una tabella.

Un terzo caso è lo shadow DOM chiuso: :has() non attraversa il confine. Se il componente è in una webview o in uno shadow root, il selettore non vede i figli.

Quando la correzione comune peggiora le cose

L'esempio più diffuso è bloccare il pulsante di invio con form:has(:invalid) button { pointer-events: none }. Sembra elegante e nasconde il vero problema: al primo caricamento la form è vuota, quindi invalida, quindi il pulsante non risponde. Chi preme Invio non riceve né il messaggio nativo del browser né un errore: il click svanisce senza spiegazione.

La variante con opacity: 0.5 è meno grave ma inganna lo stesso, perché i campi obbligatori non sono indicati e l'utente non sa cosa manca. Se usi :has() per lo stato del form, accompagnalo con :user-invalid sui campi, così il messaggio arriva quando serve, e non togliere mai i puntatori a un controllo che deve spiegare perché è bloccato.

Un limite da mettere nel conto

Il limite scritto nella specifica è l'annidamento: :has() non può stare dentro un altro :has(). Non puoi scrivere .card:has(.media:has(video)); devi passare da una classe o da due regole separate. Nella stessa famiglia rientra il divieto di usarlo dentro un pseudo-elemento: ::before non ha figli, quindi il selettore non ha un albero su cui lavorare.

Come prevenirlo

  • Usa :has() per stati visivi, non per la logica di layout critica.
  • Tieni i selettori corti: .card:has(img) è meglio di una catena di cinque livelli.
  • Documenta il fallback per le webview che non lo supportano.

Dove continuare

Nel catalogo: blocco hero, componenti e blocchi. Le guide: Flexbox e Grid per designer e UI kit, libreria o design system; il confronto: checklist di accessibilità.

Domande frequenti

Posso usarlo in produzione oggi?

Sì, se il pubblico usa browser aggiornati. Per le webview applicative datate serve un fallback con una classe aggiunta da JavaScript.

Funziona con i pseudo-elementi?

:has() non può essere usato dentro un pseudo-elemento. Va applicato a un elemento reale.

Sostituisce tutti i trucchi con JavaScript?

Quasi tutti quelli basati sui figli. Non sostituisce i casi in cui serve conoscere il contenuto o lo stato di un componente non DOM.

Perché a volte non si aggiorna?

Se il cambiamento avviene in un nodo fuori dall'albero osservato o dentro uno shadow root chiuso, il selettore non lo vede.

È più lento di una classe?

Sì, perché il browser deve valutare i discendenti. Su una card costa nulla; su una tabella con migliaia di righe conviene misurare.