# 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.

> Pagina: https://ui.eneawork.it/blog/css-selettore-genitore-has
> Markdown: https://ui.eneawork.it/blog/css-selettore-genitore-has.md

*2026-11-01*

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) { ... }.

La domanda «Is there a CSS parent selector?» vale 4334 voti su Stack Overflow (letto il 31 ottobre 2026 con la Stack Exchange API): fonte. Il catalogo EneUI misura 111.985 componenti, 1.556 blocchi, 12.808 icone da 1.472 librerie, verificati su /api/stats il 1 novembre 2026. Ogni comando qui sotto è eseguibile in locale: nessun passaggio richiede un account.

## 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

## 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

## 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à.

## FAQ

### 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.
