# :has() for form states, with no classes from JavaScript

:has() restyles a form from the state of its fields, with no class added by JavaScript. The pattern, the difference from :invalid and the real limits.

> Page: https://ui.eneawork.it/blog/css-has-stati-form
> Markdown: https://ui.eneawork.it/blog/css-has-stati-form.md

*2026-11-01*

Risposta breve: :has() permette di cambiare l'aspetto del form in base allo stato dei suoi campi, senza classi aggiunte da JavaScript. Una regola come .field:has(input:user-invalid) .hint è già il pattern completo.

Il catalogo EneUI misura 111.985 componenti, 1.556 blocchi e 12.808 icone da 1.472 librerie, valori letti da /api/stats il 1 novembre 2026. Le regole sono state verificate su Chrome e Safari correnti, con il campo compilato, svuotato e poi inviato: il messaggio compare solo dopo l'interazione. Il comando sotto si esegue in locale.

## Il pattern in quattro regole

.field:has(input:user-invalid) { --ring: crimson; } .field:has(input:user-invalid) .hint { display: block; } form:has(:user-invalid) button[type="submit"] { opacity: 0.6; } form:has(input:checked) .extra { display: block; }

## Perché non basta :invalid

:invalid scatta al primo render, prima che qualcuno tocchi un campo: il form nasce rosso e l'utente non ha fatto niente. :user-invalid aspetta l'interazione o un tentativo di invio. Dove non è supportato, tieni :invalid ma aggiungi una classe dal JavaScript che hai già, invece di un polyfill sui selettori.

## Il limite

:has() non attraversa uno shadow root chiuso e non si può usare dentro un pseudo-elemento. Se i campi stanno in un web component con shadow DOM chiuso, il selettore non vede lo stato e il pattern si ferma al confine. È il caso da verificare prima di promettere il comportamento al designer.

## Il messaggio deve esistere anche senza CSS

Mostrare il messaggio con display: none lo nasconde anche agli screen reader, e la regola che lo riapre deve essere coerente con aria-describedby. Metti il testo nel DOM, collegato al campo, e usa il CSS solo per la visibilità.

## Come verificarlo

## Dove continuare

Nel catalogo: blocchi form, blocco login e componenti. Le guide: selettore genitore con :has() e focus-visible in Tailwind; il confronto: Tailwind contro CSS Modules.

## FAQ

### Posso evitare del tutto lo stato in React?

Per la validazione visiva sì, perché il browser conosce già gli stati dei campi. Per messaggi che dipendono dalla risposta del server serve ancora lo stato.

### Qual è la differenza con :valid?

:valid è l'opposto di :invalid e ha lo stesso problema di tempismo. :user-valid e :user-invalid aspettano l'interazione, quindi non colorano un form appena aperto.

### Funziona con i campi generati da una libreria?

Sì se il campo è nel DOM della pagina. No se vive in uno shadow root chiuso, perché :has() non attraversa il confine.

### È costoso su un form lungo?

Su un form con qualche decina di campi il costo è trascurabile. Su una tabella con migliaia di input conviene misurare con il pannello Performance.

### Il bottone attenuato resta cliccabile?

Con opacity sì, ed è quello che vuoi: il click deve mostrare gli errori. Con pointer-events: none togli il feedback, quindi non usarlo da solo.
