Escribes .sidebar .card { color: blue; }, cargas la página y la tarjeta sigue en rojo por una regla que añadiste una hora antes: .card { color: red; }. Tu nueva regla va después en el archivo. Sigue perdiendo. El navegador compara los dos selectores con un sistema de puntos fijo, y .sidebar .card puntúa más alto que .card sin importar cuál escribiste al final.
Ese sistema de puntos es la especificidad. En cuanto sabes calcularla a simple vista, “por qué no se aplica mi CSS” se vuelve aritmética. No tiene nada que ver con el layout: Grid o Flexbox decide dónde se coloca un elemento, y la especificidad decide qué regla fija su color o tamaño cuando dos reglas entran en conflicto.
Cómo puntúa el navegador un selector
Cada selector tiene un peso formado por tres números, normalmente escritos como una tripleta separada por guiones: ID-clase-tipo. Suma lo que contiene el selector, columna por columna:
| Columna | Qué cuenta | Peso por coincidencia |
|---|---|---|
| ID | #header | 1-0-0 |
| Clase / atributo / pseudoclase | .card, [type="radio"], :hover, :nth-child(2) | 0-1-0 |
| Tipo / pseudoelemento | div, h1, ::before | 0-0-1 |
El selector universal * y los combinadores (el espacio, >, ~, +) suman 0-0-0. Decide qué elementos coinciden, no con qué fuerza gana la regla.
Algunos selectores reales:
| |
Para comparar dos valores, se leen de izquierda a derecha y se detiene en la primera columna que difiere. 1-1-0 gana a 0-2-0 porque la columna del ID (1 frente a 0) decide antes de que nadie mire las clases. Así, #main .card gana a .sidebar .card .title .text, sin importar lo largo que sea el segundo selector. Un ID supera a cualquier cantidad de clases.
Por qué una regla CSS más reciente puede seguir perdiendo
Volvamos al ejemplo inicial, con los números calculados:
| |
0-2-0 gana a 0-1-0 en la columna de clases, así que la tarjeta es azul sin importar el orden en el archivo. El orden en el archivo es el criterio al que casi todos recurren primero, pero solo aplica cuando las tripletas son idénticas:
| |
Ambas reglas puntúan 0-1-0, así que gana la que está más abajo en la hoja de estilos (o en el bloque <style> posterior, o en la hoja enlazada después de la otra). Es el único caso en el que “gana la última” es el modelo correcto. Funciona en proyectos pequeños, por eso es lo primero que todos aprenden, y deja de funcionar en cuanto un proyecto pasa de un puñado de selectores.
Los estilos inline están por encima de todo esto. Un atributo style="color: green" en el elemento gana a cualquier regla de cualquier hoja de estilos, por muchos IDs que encadene. Solo las declaraciones !important pueden superarlo.
| |
| |
!important no forma parte de la tripleta de especificidad. Es un nivel aparte, de prioridad más alta: cualquier declaración !important gana a cualquier declaración normal, estilos inline incluidos, sin importar el peso del selector. Cuando dos declaraciones !important chocan, decide la especificidad (y luego el orden en el archivo), igual que sin la marca. Así es como un solo !important se convierte en una carrera de armamentos. El siguiente desarrollador que necesite sobrescribirlo no tiene otra jugada disponible más que otro !important, y pronto nadie sabe qué regla debería ganar.
Qué selectores suman cero especificidad: :where() frente a :is()
Algunos selectores permiten apuntar con precisión sin pagar el precio normal.
:where() siempre aporta 0-0-0, sea lo que sea que pongas dentro:
| |
Eso convierte a :where() en el envoltorio adecuado para valores por defecto y estilos de tema que deben ser fáciles de sobrescribir. Una librería de componentes puede distribuir sus valores por defecto dentro de :where(), y una simple regla .card { } de quien la usa los gana sin necesitar !important.
:is(), :not() y :has() funcionan distinto. No suman peso propio, pero toman el peso de su argumento más específico:
| |
Así que :where() es gratis, y :is() cuesta tanto como lo más pesado que metas dentro, aunque ese argumento nunca coincida con el elemento.
Cómo las cascade layers sobrescriben la especificidad
Si estás acumulando clases e IDs para ganar peleas que no tienen nada que ver con lo específico que debería ser un selector, el proyecto necesita @layer, no más anidamiento.
| |
El orden de las layers viene de dónde declaras los nombres, no del peso de los selectores. utilities se declaró al final, así que una regla dentro de ella gana a una regla en components, aunque .text-blue tenga el mismo peso 0-1-0 que .card. La especificidad sigue resolviendo los conflictos dentro de una layer. Entre layers, el orden de las layers gana sin apelación y la especificidad nunca se consulta.
Hay una regla que atrapa a casi todos la primera vez: para declaraciones normales (sin !important), los estilos fuera de cualquier layer ganan a los estilos dentro de una layer. Mueves tu CSS de componentes a @layer components pero dejas una sola sobrescritura sin layer, y esa regla sin layer gana sin importar el orden en que declaraste las layers. Para las declaraciones !important la precedencia se invierte: las reglas important de la layer declarada primero ganan a las de las layers posteriores, y los estilos important sin layer quedan en el último lugar de todos. La inversión es deliberada, y es la parte de la especificación que conviene releer antes de depender de ella.
El soporte de navegadores dejó de ser un argumento en contra de las layers hace tiempo: @layer está disponible en todos los motores principales desde 2022. El coste real es organizativo. El equipo tiene que acordar un orden de layers de antemano (reset, base, components, utilities es uno habitual) y colocar el CSS nuevo en la layer correcta. Es más un problema de proceso que de sintaxis.
Errores comunes con la especificidad CSS
- Combatir la especificidad con más especificidad. Cada sobrescritura añade una clase o un ID más que la anterior, hasta que cambiar un color necesita
#app .page .section .card.is-active.is-selected. Es el mismo patrón de fallo que el spaghetti code: cada arreglo añade más enredo en lugar de quitarlo. La solución casi nunca es un selector más pesado. Es uno más plano, o una layer. - Recurrir a
!importanten el primer conflicto. Gana la pelea inmediata y prepara la siguiente, porque la próxima sobrescritura ya no tiene ninguna jugada de especificidad normal disponible. - Olvidar que las pseudoclases cuentan.
:hover,:focus,:nth-child()y el argumento de:not()suman todos0-1-0. Una simplea { color: blue; }más abajo en el archivo no sobrescribiráa:not(.disabled) { color: red; }escrita arriba, porque la versión con:not()pesa más. - Asumir que el valor heredado gana porque la regla “se siente” más fuerte. Una regla que apunta directamente a un elemento siempre gana a un valor heredado, sea cual sea la especificidad de la regla de la que proviene la herencia. Si un hijo muestra el color equivocado, busca una regla que apunte a ese hijo antes de sospechar de la herencia.
- Meter
:is()en un selector esperando que siga siendo barato.:is()toma el peso de su argumento más pesado, así que:is(.card, #sidebar)es en realidad un selector1-0-0disfrazado.
Cómo comprobar la especificidad en DevTools
No hace falta contar columnas a mano cuando tienes un bug delante. Inspecciona el elemento en Chrome o Firefox DevTools y abre el panel Styles (Rules en Firefox). Cada regla que coincide aparece en orden de cascada, con las declaraciones sobrescritas tachadas. Pasa el cursor sobre un selector y ambos navegadores muestran su especificidad. Desde ahí puedes saber en segundos si estás viendo una diferencia de especificidad, un problema de orden de layers, o simple orden en el archivo entre dos reglas del mismo peso.
La próxima vez que una regla pierda sin motivo aparente, calcula las dos tripletas antes de añadir una clase o un !important para forzarla. Normalmente el arreglo correcto es quitarle peso al selector que está ganando, no acumular más en el que debería haber ganado.