Especificidad: tres columnas que no se suman
Qué mide realmente la especificidad, por qué se compara columna a columna en lugar de como un número, y qué desapareció del sistema cuando se reescribió la especificación.
La especificidad es el quinto criterio de la cascada y el único que mide una propiedad del selector en lugar de una decisión del autor. Su definición cabe en un párrafo, y sin embargo se explica mal de forma sistemática: como un número de tres cifras que se suma, como una puntuación en la que los estilos en línea valen mil, o como el criterio principal de la cascada. Ninguna de las tres cosas es cierta, y las tres producen predicciones equivocadas en cuanto el CSS se complica.
- Enunciar qué cuenta en cada una de las tres columnas de la especificidad.
- Comparar dos especificidades con el criterio correcto, columna a columna.
- Explicar por qué el atributo
styleya no forma parte del sistema. - Justificar qué intención pretende aproximar la métrica y dónde falla.
Las tres columnas
La especificidad de un selector es una terna de números, que se suele escribir separada por guiones. Se calcula contando componentes:
| Columna | Qué cuenta | Ejemplos |
|---|---|---|
| A | Selectores de identificador | #cabecera |
| B | Clases, selectores de atributo y pseudo-clases | .activo, [href], :hover, :nth-child(2) |
| C | Selectores de tipo y pseudo-elementos | div, p, ::before, ::marker |
Y hay dos cosas que no cuentan nada: el selector universal * y todos los combinadores, es decir el espacio del descendiente, el > del hijo, el + del hermano adyacente y el ~ del hermano general. Un selector puede tener diez combinadores y seguir valiendo cero.
/* 0-0-0 — el universal y los combinadores no puntúan */
* > * + * { margin-block-start: 1em; }
/* 0-0-2 */
article p { line-height: 1.6; }
/* 0-1-1 */
p.destacado { font-weight: 600; }
/* 0-2-0 */
[data-estado="abierto"]:hover { color: crimson; }
/* 1-0-0 */
#principal { max-inline-size: 70ch; }
La agrupación de la columna B tiene una lógica que ayuda a recordarla: clases, atributos y pseudo-clases seleccionan conjuntos de elementos por una característica. La columna C agrupa lo que selecciona por categoría estructural: qué clase de elemento es, o qué parte generada de un elemento. Y la columna A queda reservada a lo que, en un documento bien formado, selecciona exactamente uno.
Esa progresión —categoría, característica, individuo— es la intención que la métrica trata de capturar. Cuanto más estrecho es el conjunto al que apuntas, más concreta se supone que es tu intención, y por tanto más razonable es que ganes.
Por qué no se suman
Aquí está el error de modelo más costoso. La especificidad no es un número en base diez y las columnas no se suman entre sí. La comparación es lexicográfica: se mira la columna A de las dos; si difieren, ya está decidido; si empatan, se pasa a la B; y solo si también empata, a la C.
La consecuencia es contundente:
/* 0-0-13 */
html body main article section div ul li p span a em strong { color: red; }
/* 0-1-0 */
.enlace { color: blue; }
Gana blue. Trece selectores de tipo pierden contra una sola clase, porque la columna B se compara antes que la C y no hay acarreo posible de una columna a otra. La imagen mental correcta no es la de un número: es la de tres cifras que se comparan por separado, como se ordenan las versiones de un programa, donde una versión 2.0.0 es posterior a una 1.99.99.
De ahí salen dos reglas prácticas inmediatas. La primera: añadir tipos nunca te va a servir para ganarle a una clase. Poner div delante de tu selector es un gesto ritual que no cambia nada relevante. La segunda: la única forma de superar un identificador es otro identificador, y ninguna cantidad de clases lo consigue.
Aquí está el defecto de fondo de la métrica, y conviene verlo con claridad porque explica por qué la disciplina de arquitectura CSS existe. La especificidad es una heurística sobre la forma sintáctica del selector, y la usamos para responder a una pregunta sobre la intención de quien lo escribió. Casi siempre correlacionan, y por eso el sistema funciona razonablemente en un fichero pequeño. Pero se rompe en cuanto el selector se escribe por otro motivo. Piensa en el caso más habitual: .tarjeta .titulo tiene especificidad 0-2-0 no porque quien lo escribió pretendiera imponerse a nadie, sino porque necesitaba acotar el alcance a los títulos que están dentro de tarjetas. Acotar el alcance y reclamar autoridad son dos intenciones distintas, y la métrica las confunde en un único número. El resultado es que acotar penaliza: cuanto mejor delimitas dónde se aplica una regla, más difícil se lo pones a quien luego quiera modificarla. Es un incentivo perverso perfecto, porque premia escribir selectores vagos. La reparación llegó por dos vías complementarias y ambas atacan exactamente ese punto: :where() permite acotar sin puntuar, separando por fin las dos intenciones que la métrica mezclaba; y @layer permite declarar autoridad sin tocar el selector, sacando esa decisión del terreno de la especificidad. Con las dos en la mano, la especificidad puede volver a hacer lo único que hace bien, que es desempatar entre reglas hermanas escritas con el mismo criterio. Todo intento de usarla para más que eso está peleando contra el diseño de la métrica.
Lo que ya no está en el sistema
En las especificaciones antiguas la especificidad tenía cuatro componentes, y el primero valía 1 para las declaraciones del atributo style y 0 para todo lo demás. De ahí viene la frase, todavía muy repetida, de que “los estilos en línea valen 1000”.
Ese componente ya no forma parte de la especificidad. Los estilos pegados al elemento se comparan en un criterio propio de la cascada, situado por encima de las capas y por tanto muy por encima de la especificidad. El cambio no es cosmético: con el modelo viejo podías imaginar que un selector suficientemente elaborado alcanzaría a un estilo en línea, y con el modelo real eso es imposible por construcción, porque la comparación ni siquiera llega al escalón donde vive la especificidad.
Hay una segunda leyenda de la misma época que conviene enterrar: la del desbordamiento entre columnas. En navegadores muy antiguos las columnas se almacenaban en campos pequeños y con suficientes selectores se producía un acarreo real, de modo que 256 clases podían comportarse como un identificador. En los motores actuales no ocurre: no hay acarreo entre columnas y los valores no se saturan a ningún tope que puedas alcanzar escribiendo CSS.
Dónde vive la especificidad y dónde no
Tres precisiones que evitan errores de razonamiento.
La especificidad es del selector, no de la regla. En una regla con lista de selectores separados por comas, cada selector tiene la suya, y cada elemento compara con la del selector que casó con él. Estas dos declaraciones no tienen la misma fuerza aunque estén en la misma regla:
h2, .titulo-seccion #principal { margin-block: 0; }
Para un h2 esa regla vale 0-0-1; para el elemento que case con el segundo selector vale 1-1-0.
La especificidad no se hereda. Un valor que llega por herencia no arrastra ninguna especificidad, porque ni siquiera participa en la cascada. Cualquier declaración sobre el elemento hijo, por débil que sea, gana a un valor heredado.
La especificidad no depende del elemento. Se calcula a partir del texto del selector, siempre igual, y no cambia según cuántos elementos case ni cuán profundo esté ninguno. Es una propiedad estática del selector, calculable sin mirar el documento. Esa es exactamente la razón de que sea tan barata de evaluar, y también la de que sea tan mala aproximación de la intención.