Scrivi .sidebar .card { color: blue; }, carichi la pagina e la card è ancora rossa, per colpa di una regola aggiunta un’ora prima: .card { color: red; }. La nuova regola viene dopo nel file. Perde comunque. Il browser confronta i due selettori con un sistema a punti fisso, e .sidebar .card ottiene un punteggio più alto di .card, indipendentemente da chi è stato scritto per ultimo.
Quel sistema a punti è la specificità. Una volta che sai calcolarla a colpo d’occhio, “perché il mio CSS non si applica” diventa aritmetica. Non ha niente a che fare con il layout: Grid o Flexbox decide dove si posiziona un elemento, la specificità decide quale regola ne imposta colore o dimensione quando due regole sono in conflitto.
Come il browser assegna il punteggio a un selettore
Ogni selettore ha un peso composto da tre numeri, di solito scritti come tripletta separata da trattini: ID-classe-tipo. Somma cosa contiene il selettore, colonna per colonna:
| Colonna | Cosa conta | Peso per occorrenza |
|---|---|---|
| ID | #header | 1-0-0 |
| Classe / attributo / pseudo-classe | .card, [type="radio"], :hover, :nth-child(2) | 0-1-0 |
| Tipo / pseudo-elemento | div, h1, ::before | 0-0-1 |
Il selettore universale * e i combinatori (lo spazio, >, ~, +) aggiungono 0-0-0. Decidono quali elementi corrispondono, non quanto forte vince la regola.
Alcuni selettori reali:
| |
Per confrontare due valori, si leggono da sinistra a destra e ci si ferma alla prima colonna diversa. 1-1-0 batte 0-2-0 perché la colonna ID (1 contro 0) decide prima che qualcuno guardi le classi. Così #main .card batte .sidebar .card .title .text, per quanto lungo diventi il secondo selettore. Un ID supera qualsiasi numero di classi.
Perché una regola CSS più recente può comunque perdere
Torniamo all’esempio iniziale, con i numeri calcolati:
| |
0-2-0 batte 0-1-0 nella colonna delle classi, quindi la card è blu indipendentemente dall’ordine nel file. L’ordine nel file è il criterio a cui quasi tutti pensano per primo, ma vale solo quando le triplette sono identiche:
| |
Entrambe le regole hanno punteggio 0-1-0, quindi vince quella scritta più avanti nel foglio di stile (o nel blocco <style> successivo, o nel foglio collegato dopo l’altro). È l’unico caso in cui “vince l’ultima” è il modello corretto. Funziona nei progetti piccoli, per questo è la prima cosa che si impara, e smette di funzionare non appena un progetto supera una manciata di selettori.
Gli stili inline stanno sopra a tutto questo. Un attributo style="color: green" sull’elemento vince su ogni regola in ogni foglio di stile, per quante ID concatenino. Solo le dichiarazioni !important possono batterlo.
| |
| |
!important non fa parte della tripletta di specificità. È un livello separato, a priorità più alta: qualsiasi dichiarazione !important batte qualsiasi dichiarazione normale, stili inline compresi, a prescindere dal peso del selettore. Quando due dichiarazioni !important sono in conflitto, decide la specificità (poi l’ordine nel file), esattamente come senza il flag. È così che un singolo !important si trasforma in una corsa agli armamenti. Il prossimo sviluppatore che deve sovrascriverlo non ha più mosse disponibili a parte un altro !important, e presto nessuno sa più quale regola dovrebbe vincere.
Quali selettori aggiungono zero specificità: :where() e :is()
Alcuni selettori permettono di mirare con precisione senza pagare il prezzo normale.
:where() contribuisce sempre 0-0-0, qualunque cosa ci metti dentro:
| |
Questo rende :where() il wrapper giusto per valori predefiniti e stili di tema che devono essere facili da sovrascrivere. Una libreria di componenti può fornire i suoi default dentro :where(), e una semplice regola .card { } scritta da chi la usa li batte senza bisogno di !important.
:is(), :not() e :has() funzionano diversamente. Non aggiungono peso proprio, ma prendono il peso del loro argomento più specifico:
| |
Quindi :where() è gratuito, mentre :is() costa quanto la cosa più pesante che ci metti dentro, anche se quell’argomento non corrisponde mai all’elemento.
Come i cascade layer sovrascrivono la specificità
Se stai accumulando classi e ID per vincere scontri che non hanno niente a che fare con quanto specifico dovrebbe essere un selettore, il progetto ha bisogno di @layer, non di altro nesting.
| |
L’ordine dei layer viene da dove dichiari i nomi, non dal peso dei selettori. utilities è stato dichiarato per ultimo, quindi una regola al suo interno batte una regola in components, anche se .text-blue ha lo stesso peso 0-1-0 di .card. La specificità continua a risolvere i conflitti dentro un layer. Tra layer diversi, l’ordine dei layer vince senza appello e la specificità non viene mai consultata.
Una regola prende quasi tutti alla sprovvista la prima volta: per le dichiarazioni normali (senza !important), gli stili fuori da ogni layer battono gli stili dentro un layer. Sposta il CSS dei componenti in @layer components ma lascia una sola sovrascrittura fuori dai layer, e quella regola non layerizzata vince indipendentemente dall’ordine in cui hai dichiarato i layer. Per le dichiarazioni !important la precedenza si inverte: le regole important del layer dichiarato per primo battono quelle dei layer successivi, e gli stili important non layerizzati si classificano come i più debole di tutti. L’inversione è voluta, ed è la parte della specifica da rileggere prima di farci affidamento.
Il supporto dei browser ha smesso di essere un argomento contro i layer da un po’: @layer è disponibile in tutti i motori principali dal 2022. Il costo reale è organizzativo. Il team deve concordare in anticipo un ordine dei layer (reset, base, components, utilities è uno schema comune) e inserire il nuovo CSS nel layer giusto. È più un problema di processo che di sintassi.
Errori comuni con la specificità CSS
- Combattere la specificità con più specificità. Ogni sovrascrittura aggiunge una classe o un ID in più rispetto alla precedente, finché cambiare un colore richiede
#app .page .section .card.is-active.is-selected. È lo stesso schema di guasto dello spaghetti code: ogni correzione aggiunge groviglio invece di rimuoverlo. La soluzione quasi mai è un selettore più pesante. È uno più piatto, o un layer. - Ricorrere a
!importantal primo conflitto. Vince lo scontro immediato e prepara il prossimo, perché la prossima sovrascrittura non ha più mosse di specificità normale disponibili. - Dimenticare che le pseudo-classi contano.
:hover,:focus,:nth-child()e l’argomento di:not()aggiungono tutti0-1-0. Una semplicea { color: blue; }più avanti nel file non sovrascriveràa:not(.disabled) { color: red; }scritta sopra, perché la versione con:not()è più pesante. - Pensare che il valore ereditato vinca perché la regola “sembra” più forte. Una regola che punta direttamente a un elemento batte sempre un valore ereditato, qualunque sia la specificità della regola da cui proviene l’eredità. Se un elemento figlio mostra il colore sbagliato, cerca una regola che punta a quel figlio prima di sospettare dell’eredità.
- Mettere
:is()dentro un selettore aspettandosi che resti leggero.:is()prende il peso del suo argomento più pesante, quindi:is(.card, #sidebar)è di fatto un selettore1-0-0camuffato.
Come controllare la specificità in DevTools
Non serve contare colonne a mano quando un bug è già davanti a te. Ispeziona l’elemento in Chrome o Firefox DevTools e apri il pannello Styles (Rules in Firefox). Ogni regola che corrisponde è elencata in ordine di cascata, con le dichiarazioni sovrascritte mostrate con una riga sopra. Passa il mouse su un selettore e entrambi i browser mostrano la sua specificità. Da lì puoi capire in pochi secondi se stai guardando una differenza di specificità, un problema di ordine dei layer, o semplice ordine nel file tra due regole di peso uguale.
La prossima volta che una regola perde senza spiegazione apparente, calcola entrambe le triplette prima di aggiungere una classe o un !important per forzarla. Di solito la correzione giusta è togliere peso al selettore che sta vincendo, non accumularne altro su quello che avrebbe dovuto vincere.