Calcolo della specificità dei selettori CSS

Quando più regole CSS mirano allo stesso elemento e impostano la stessa proprietà (per esempio color), il browser deve stabilire quale dichiarazione “vince”. La scelta segue la cascata e dipende, nell’ordine, da: importanza e origine, specificità del selettore e ordine di apparizione nel codice. Questo articolo si concentra sulla specificità: come si calcola, come si confronta e quali casi particolari possono sorprendere.

Che cos’è la specificità

La specificità è un punteggio associato a un selettore. Più è alto, più quella regola ha priorità rispetto ad altre regole concorrenti (a parità di importanza e origine). La specificità non è “la potenza del selettore”, ma un criterio numerico di confronto usato dal browser.

La cascata in breve (per capire dove si inserisce la specificità)

  1. Importanza e origine: regole dell’autore, dell’utente, dell’agente utente (browser), e la presenza di !important.
  2. Specificità: tra regole con stessa importanza e stessa origine, vince la regola col selettore più specifico.
  3. Ordine nel sorgente: se la specificità è identica, vince la regola scritta più in basso (più recente).

Nota: !important cambia il livello di priorità nella cascata, ma non modifica il calcolo della specificità. Una volta stabilito che due dichiarazioni sono sullo stesso piano (per esempio entrambe !important dello stesso autore), la specificità torna a essere determinante.

Il modello di calcolo: la “tupla” (a, b, c, d)

Il modo più chiaro per ragionare è usare una tupla a quattro componenti (a, b, c, d):

  • a: stili inline (attributo style="" nell’HTML)
  • b: numero di selettori ID (#id)
  • c: numero di classi (.classe), attributi ([type="text"]) e pseudo-classi (:hover, :nth-child(), ...)
  • d: numero di selettori di tipo (tag: p, h1, input, ...) e pseudo-elementi (::before, ::marker, ...)

Si confrontano le tuple in modo lessicografico: prima a, poi b, poi c, poi d. Il primo valore in cui differiscono decide il vincitore. Non esiste “somma totale” che valga più del confronto per colonne.

Esempio rapido

Confrontiamo #menu a e .nav a:

  • #menu a(0, 1, 0, 1)
  • .nav a(0, 0, 1, 1)

Vince #menu a perché ha più ID (colonna b).

Cosa conta e cosa non conta nel selettore

Conta

  • ID: #sidebar
  • Classi: .card
  • Selettori di attributo: [disabled], [data-state="open"]
  • Pseudo-classi: :hover, :focus, :not() (con regole specifiche), :is(), :has(), ecc.
  • Selettori di tipo: button, ul, svg
  • Pseudo-elementi: ::before, ::after, ::placeholder, ::marker

Non conta (o non aggiunge specificità)

  • Il combinatore: spazio ( ), >, +, ~ non aggiungono punteggio.
  • Il selettore universale * non aggiunge punteggio.
  • I selettori di colonna (||, in contesti tabellari) non aggiungono punteggio.

Tabella pratica di conteggio

Una regola operativa utile è: “conta i pezzi” e mettili nelle colonne giuste. Ecco alcuni esempi con la loro tupla:

/* (0, 0, 0, 1) */
p { color: #333; }

/* (0, 0, 1, 0) */
.warning { color: #b00; }

/* (0, 1, 0, 0) */
#banner { color: #036; }

/* (0, 0, 2, 1): .card + [data-variant] + h2 */
h2.card[data-variant="promo"] { color: #084; }

/* (0, 0, 1, 2): ul + li + :first-child */
ul li:first-child { font-weight: bold; }

Specificità e selettori composti: somma per colonne

In un selettore composto, ogni parte valida contribuisce. Per esempio, a.button.primary:hover è:

  • a → tipo: d += 1
  • .button → classe: c += 1
  • .primary → classe: c += 1
  • :hover → pseudo-classe: c += 1

Totale: (0, 0, 3, 1).

Casi speciali e pseudo-classi “funzionali”

:not()

:not() non “vale zero”: contribuisce con la specificità dell’argomento più specifico al suo interno. In altre parole, il contenuto di :not() viene considerato nel conteggio. Esempio:

/* :not(.disabled) contribuisce come una classe (0,0,1,0) */
button:not(.disabled) { opacity: 1; }

/* :not(#x) contribuisce come un ID (0,1,0,0) */
button:not(#x) { outline: 2px solid; }

:is() e :has()

Per :is() e :has() la regola pratica è simile: la specificità del selettore complessivo include quella dell’argomento più specifico tra le alternative passate. Questo è importante perché :is() consente di scrivere selettori compatti senza perdere “potere” quando una delle alternative è molto specifica.

/* Specificità determinata dall'opzione più specifica dentro :is() */
:is(.btn, #cta) { padding: 1rem; }      /* (0,1,0,0) per via di #cta */

/* :has() conta la specificità del selettore più specifico nel suo argomento */
.card:has(img[alt]) { border: 1px solid; } /* (0,0,2,1): .card + :has(img[alt]) */

:where() (il caso “a specificità zero”)

:where() è progettata per non aggiungere specificità: qualunque cosa contenga, il suo contributo è (0, 0, 0, 0). È utilissima per scrivere selettori “ampi” in CSS di base (reset, design system) che poi possono essere sovrascritti facilmente da regole più mirate.

/* :where(...) non aumenta la specificità */
:where(nav a) { text-decoration: none; } /* (0,0,0,0) */

/* Questa regola vince facilmente perché ha (0,0,1,1) */
nav a.link { text-decoration: underline; }

Stili inline e il valore di a

Gli stili inline (attributo style="") si comportano come se avessero a = 1, quindi vincono contro qualsiasi selettore scritto in un foglio CSS “normale” (sempre a parità di importanza). Esempio:

<p style="color: red;" class="note">Testo</p>

Anche se esiste una regola come #content .note { color: blue; } (che è molto specifica), lo stile inline color: red prevale perché è nella colonna a.

Quando la specificità non basta: ordine nel sorgente

Se due regole hanno la stessa specificità, vince quella che appare più tardi. Questo accade spesso con selettori identici ripetuti in file diversi o in moduli concatenati.

.tag { color: #444; }
.tag { color: #111; } /* vince perché è dopo */

Specificità vs !important

!important può “saltare” sopra regole altrimenti più specifiche non importanti. Tuttavia, se entrambe le regole sono !important (e di pari origine), allora vince quella con specificità maggiore; se la specificità è identica, torna a contare l’ordine nel sorgente.

.box { width: 10rem !important; }     /* (0,0,1,0) importante */
#panel .box { width: 12rem; }         /* (0,1,1,0) non importante: perde */

#panel .box { width: 12rem !important; } /* importante: ora vince per specificità */

Algoritmo pratico: come calcolare a colpo sicuro

  1. Ignora combinatori ( , >, +, ~) e il selettore universale *.
  2. Conta gli stili inline (quasi sempre 0 nei fogli CSS).
  3. Conta gli ID.
  4. Conta classi, attributi e pseudo-classi.
  5. Conta tipi e pseudo-elementi.
  6. Forma la tupla e confrontala con quella delle regole concorrenti, da sinistra a destra.

Esercizi guidati

Esercizio 1

Selettori:

  • ul#menu li a
  • #menu .item a

Calcolo:

  • ul#menu li aul (tipo) + #menu (ID) + li (tipo) + a (tipo) → (0,1,0,3)
  • #menu .item a#menu (ID) + .item (classe) + a (tipo) → (0,1,1,1)

Confronto: (0,1,0,3) vs (0,1,1,1). Le colonne a e b sono uguali, ma c differisce: 0 contro 1. Vince #menu .item a.

Esercizio 2

Selettori:

  • .card :where(h2)
  • .card h2

:where(h2) non aggiunge specificità. Quindi:

  • .card :where(h2) → solo .card(0,0,1,0)
  • .card h2.card (classe) + h2 (tipo) → (0,0,1,1)

Vince .card h2.

Consigli di progettazione CSS per evitare “guerre di specificità”

  • Preferisci classi per comporre componenti e varianti; evita catene lunghe di selettori di tipo.
  • Usa gli ID con parsimonia nei selettori CSS: aumentano molto la specificità e rendono più difficile sovrascrivere.
  • Evita l’abuso di !important: spesso maschera problemi di architettura e rende fragile la manutenzione.
  • Valuta :where() per stili base a bassa specificità (reset, tipografia, layout generali).
  • Isola le responsabilità: componenti con classi dedicate riducono la necessità di selettori complessi.

Riepilogo

La specificità si calcola contando gli elementi del selettore e distribuendoli nella tupla (a, b, c, d). Il confronto avviene da sinistra a destra: stili inline, poi ID, poi classi/attributi/pseudo-classi, infine tipi/pseudo-elementi. Conoscere i casi particolari (:where(), :is(), :not(), :has()) permette di scrivere CSS più prevedibile e facilmente sovrascrivibile.

Torna su