Sviluppo web
:has() per gli stati del form, senza classi da JavaScript
:has() cambia l'aspetto del form in base allo stato dei campi, senza classi da JavaScript. Il pattern, la differenza con :invalid e i limiti reali.
In sintesi
- :has() legge lo stato dei campi e lo applica al contenitore: il form si colora senza una riga di stato in JavaScript.
- :invalid scatta al primo render; :user-invalid aspetta che l'utente tocchi il campo.
- Su shadow root chiuso il selettore non vede i campi: lì serve la classe dal JavaScript.
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 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;
}
| Stato | Selettore | Effetto |
|---|---|---|
| Campo non valido dopo l'interazione | .field:has(:user-invalid) | bordo e messaggio |
| Almeno un campo non valido | form:has(:user-invalid) | bottone attenuato |
| Opzione selezionata | form:has(input:checked) | mostra il campo extra |
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
- Compila un campo, svuotalo e sposta il focus: il messaggio deve apparire dopo, non prima.
- Invia con un campo vuoto: il bottone deve restare raggiungibile da tastiera, anche attenuato.
- Prova con lo screen reader: il messaggio deve essere letto, non solo visibile.
- Disattiva il CSS: il form deve restare compilabile e comprensibile.
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.
Domande frequenti
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 <code>opacity</code> sì, ed è quello che vuoi: il click deve mostrare gli errori. Con <code>pointer-events: none</code> togli il feedback, quindi non usarlo da solo.