Cómo elige el Cascade al ganador: importancia, especificidad y orden de las fuentes
Vea cómo los navegadores resuelven las declaraciones CSS conflictivas, cómo leer la especificidad como una comparación de cuatro partes, y por qué las reglas hover y !important a menudo le sorprenden.
Cuando varias reglas CSS apuntan al mismo elemento y establecen la misma propiedad, el navegador no puede aplicarlas todas. Necesita una forma determinista para elegir exactamente una declaración, y ese proceso explica toda una categoría de errores del tipo “¿por qué se ignora mi estilo?”. Al final de esta guía podrás calcular la especificidad de un selector, predecir qué declaración prevalecerá y depurar casos difíciles como una regla :hover que nunca se activa, sin tener que recurrir a !important.
Dónde encaja esto en el proceso de renderizado
A un nivel alto, el navegador convierte el marcado y los estilos en píxeles a través de una serie de etapas. El HTML se analiza para formar el DOM, el CSS se convierte en CSSOM, ambos se combinan en un árbol de renderizado, y luego el proceso de maquetación y dibujo genera lo que ves:
HTML
↓
DOM
↓
CSS
↓
CSSOM
↓
Render Tree
↓
Layout
↓
Paint
↓
Pixels
Dentro del proceso CSS se esconden tres preguntas relacionadas. Primero, cuando varias declaraciones compiten, ¿cuál gana? Segundo, una vez elegido el ganador, ¿a qué valor se resuelve realmente su valor? Tercero, ¿qué ocurre cuando un elemento no tiene ningún valor para una propiedad? Las respuestas son la cascada (con la especificidad como eje central), el procesamiento de valores y la herencia. A menudo se enseñan como temas independientes, pero dentro del navegador son pasos consecutivos de la misma tarea. Esta guía se centra en la primera pregunta. Si desea más contexto sobre lo que sucede después de que se resuelvan los estilos, consulte cómo pinta el navegador y dónde encaja React.
Reglas, declaraciones y valores competidores
Primero, un poco de vocabulario. Una regla CSS está formada por un seleccionador seguido de un bloque de declaración:
.button {
background-color: blue;
}
En esa regla, .button es el seleccionador, background-color: blue; es una declaración, background-color es la propiedad y blue es el valor. El valor tal como lo escribiste se denomina valor declarado.
Una hoja de estilos real suele contener varias declaraciones para la misma propiedad en el mismo elemento. Una regla podría dirigirse a todos los botones:
button {
background-color: red;
}
mientras que otras, quizás en un archivo diferente, se dirigen a los botones en general y a un botón específico por su id:
button {
background-color: blue;
}
#submit {
background-color: green;
}
Si un único <button id="submit"> cumple con los tres requisitos, ¿qué color de fondo debería tener? Resolver esto es la función del enfoque en cascada. Este resuelve conflictos analizando la importancia de cada declaración, la especificidad de su selector y el orden en que aparecen las declaraciones. Una vez considerada la importancia, generalmente es la especificidad la que determina el resultado.
El enfoque en cascada completo según la especificación también tiene en cuenta el origen de una hoja de estilo (valores predeterminados del navegador, estilos del usuario, estilos del autor) y, en el CSS moderno, las capas de enfoque en cascada declaradas con @layer. En las hojas de estilo comunes sin capas, la importancia, la especificidad y el orden de origen son los tres factores que se deben considerar.
Qué mide la especificidad
La especificidad es la medida que utiliza el navegador para determinar con qué precisión un selector apunta a un elemento. No todos los selectores tienen el mismo peso. Compare un selector de tipo:
p {
color: red;
}
un selector de clase:
.text {
color: blue;
}
y un selector id:
#title {
color: green;
}
Si los tres coinciden con el mismo elemento, el navegador los clasifica según una jerarquía fija de tipos de selectores, del más fuerte al más débil:
Inline styles
↓
IDs
↓
Classes / pseudo-classes / attributes
↓
Elements / pseudo-elements
Cuando las declaraciones en competencia tienen la misma importancia, gana el selector más específico. En este caso, ganaría la regla id y el texto se mostraría en verde.
Una aclaración importante en la fila superior: los estilos inline definidos mediante el atributo style no son selectores, y la especificación actual los trata como un paso separado que tiene prioridad sobre cualquier declaración de autor basada en selectores. Al modelarlos como la “columna” más alta de especificidad, como se hace comúnmente, se obtiene el mismo resultado en la práctica; por eso el resto de esta guía mantiene ese modelo conveniente.
Interpretar la especificidad como cuatro columnas
El error conceptual más frecuente es pensar que la especificidad es una sola puntuación que se puede sumar. Es mejor entenderla como un conjunto de cuatro valores, uno por categoría:
Inline | IDs | Classes | Elements
Para un selector dado, se cuenta cuántas partes pertenecen a cada categoría. Un selector de clase simple es el caso más sencillo:
.button {
background: blue;
}
No contiene estilos inline, ni id, solo una clase y sin elementos:
Inline styles → 0
IDs → 0
Classes → 1
Elements → 0
lo cual se puede escribir de forma compacta como:
0, 0, 1, 0
Ahora consideremos un selector más complejo:
nav#main .button div {
background: green;
}
Contiene un id (#main), una clase (.button) y dos selectores de tipo (nav y div); por lo tanto, su especificidad es:
0, 1, 1, 2
Para comparar dos selectores, el navegador lee estas tuplas de izquierda a derecha, comenzando por la columna más significativa. La primera columna en la que difieren determina el resultado, y las columnas a su derecha ya no tienen importancia. Por eso un único id supera a cualquier cantidad de clases, y una única clase supera a cualquier cantidad de selectores de tipo: no hay transferencia de valores de una columna a otra, por lo que diez clases nunca “suman” el valor de un id. No es necesario memorizar aritmética; basta recordar el orden de comparación.
Analizando un conflicto real
Considere un botón que tiene tanto una clase como un id. Tenga en cuenta que se trata de marcado HTML puro, por lo que utiliza class en lugar de className de JSX:
<button class="button" id="submit">
Don't Click
</button>
Ahora supongamos que la hoja de estilos contiene una regla de clase:
.button {
background: blue;
}
además de varias otras, incluyendo un selector de tipo, un selector de descendiente largo y una regla id-plus-class con estado hover:
button {
background: purple;
}
nav#main .button div {
background: green;
}
#submit.button:hover {
background: yellow;
}
Todas ellas declaran background, por lo que compiten entre sí. El navegador no simplemente elige la que aparezca por último; primero compara su especificidad.
Hay un detalle que es fácil pasar por alto en ese bloque: nav#main .button div en realidad apunta a un div anidado dentro de un elemento con la clase button, no al propio botón. Dado que el sujeto de un selector es su parte más a la derecha, esta regla nunca coincide con nuestro <button>, sea cual sea su especificidad. Verificar si una regla coincide en absoluto es siempre el paso cero de la depuración.
En cuanto a las reglas que sí coinciden, compare un selector de clase:
.button
con un selector de tipo simple:
button
El primero tiene una clase, el segundo solo un elemento. Por lo tanto:
.button
tiene prioridad:
button
y el botón es azul, no morado, aunque la regla de color morado aparece más tarde.
Agregue un id al conjunto:
#submit.button
El id coloca a este selector en una columna más alta que cualquier otro formado únicamente por clases y tipos. Al comparar de izquierda a derecha, la columna del id decide inmediatamente el resultado, y por eso un id puede superar a un selector compuesto por muchas clases.
Por qué una regla :hover correcta puede no funcionar
Este caso causa mucha confusión durante la depuración. Comience con una regla base para el botón:
#submit.button {
background: red;
}
y una regla hover para el mismo elemento:
#submit.button:hover {
background: yellow;
}
La regla hover tiene un id, una clase y una pseudo-clase, lo que le da más especificidad que la regla base; por eso, al pasar el cursor sobre ella, el botón se vuelve amarillo como era de esperar.
Ahora imagine que otra parte del código estiliza al mismo botón mediante un selector mucho más largo:
nav#main div#container #submit.button {
background: red;
}
mientras que la regla hover permanece sin cambios:
#submit.button:hover {
background: yellow;
}
La regla hover sigue incluyendo una pseudo-clase:
:hover
Las pseudo-clases sí se cuentan en la columna de clases. Pero eso solo suma un punto a nivel de clase. El selector largo contiene tres IDs frente al uno de la regla hover, por lo que gana en la columna de IDs antes incluso de comparar las clases. El resultado es una regla :hover que es sintácticamente perfecta, coincide con el elemento y, aun así, no cambia nada en la pantalla.
La lección es que cuando los estados de interacción parecen estar defectuosos, rara vez la pseudo-clase es la culpable. El problema real suele ser que alguna otra declaración es más específica. Las herramientas de desarrollo del navegador lo hacen visible: el panel Estilos lista todas las reglas que coinciden y tacha con una línea las declaraciones que perdieron, lo cual te indica exactamente qué selector está superando al tuyo.
Los empates se resuelven por el orden de origen
A veces dos selectores tienen una especificidad idéntica. Tomemos dos reglas con el mismo selector de clase:
.button {
background: red;
}
.button {
background: blue;
}
Son igualmente específicos e igualmente importantes, por lo que ninguno de los dos primeros niveles puede decidir. El navegador recurre entonces al orden de origen: gana la declaración que aparece más tarde. Con este orden:
.button {
background: red;
}
.button {
background: blue;
}
el botón termina siendo azul.
Todo el proceso de toma de decisiones puede imaginarse como una secuencia de criterios de desempate:
Importance
↓
Specificity
↓
Source Order
Cada nivel solo se consulta si el anterior no pudo determinar un ganador. Primero cuenta la importancia; si hay empate, decide la especificidad; si también hay empate, gana la última declaración.
El costo de recurrir a !important
Casi todos los desarrolladores han utilizado esta solución de emergencia al menos una vez:
color: red !important;
Agregar !important aumenta la importancia de una declaración. Dado que se verifica la importancia antes que la especificidad, una declaración marcada de esta manera puede superar a otra con mucha mayor especificidad. Por ejemplo:
.button {
background: purple !important;
}
vencerá a una declaración normal en un selector largo con muchos identificadores. Cuando dos declaraciones !important compiten, el navegador vuelve a comparar su especificidad y luego su orden.
Ese poder es precisamente lo que lo hace arriesgado. Una espiral de depuración común se ve así:
"My style isn't working."
↓
"Let's increase the specificity."
↓
"Still not working."
↓
"Let's add !important."
↓
"It works!"
El estilo finalmente se muestra, pero el conflicto subyacente no ha desaparecido; se ha trasladado a la siguiente persona que necesite sobrescribir esa propiedad y ahora tiene que lidiar con su propio !important. A medida que se acumulan más de estos casos, es cada vez más difícil comprender la hoja de estilos. Trate !important como un último recurso, y considere una necesidad repentina de usarlo como una señal de que el CSS probablemente necesita ser refactorizado.
Antes de escribir:
!important
haga una pregunta más útil: ¿por qué mi declaración no prevalece? Luego verifique los niveles en orden:
- ¿Es la declaración competidora más importante?
- ¿Es el selector competidor más específico?
- ¿La declaración competidora aparece más tarde en el código fuente?
Sí existen usos legítimos, como las clases utilitarias destinadas a aplicarse siempre o para sobrescribir estilos en línea inyectados por un widget de terceros que no se puede modificar, pero deberían ser algo deliberado y no algo automático.
Escribir selectores que funcionen de forma natural
Aquí hay un aspecto más amplio relacionado con el mantenimiento. Cuando un estilo no se aplica, es tentador hacer que el selector sea cada vez más largo hasta que sí se aplique:
body div section nav ul li a.button {
color: red;
}
Funciona, pero cada parte adicional eleva las expectativas para cualquier sobrescritura futura, vincula el estilo a una estructura DOM específica y hace que la hoja de estilos sea más difícil de leer. En lugar de preguntarse cómo hacer que un selector funcione a cualquier costo, hay que preguntarse cómo organizar el CSS para que la declaración prevista funcione por sí sola. Mantener la mayoría de los selectores en una única clase, evitar usar identificadores para el estilo y agrupar las sobrescrituras más específicas cerca de las reglas que modifican ayuda mucho.
Esto es especialmente importante en grandes bases de código donde muchas personas escriben CSS. La especificidad tiene como objetivo hacer que los resultados sean predecibles, no convertirse en una carrera armamentística entre selectores.
Uso del orden de los archivos fuente con hojas de estilo de terceros
El orden de los archivos fuente se convierte en una herramienta práctica cuando combinas tus propios estilos con un archivo de reset o con una hoja de estilo de terceros. Esos archivos suelen establecer estilos para elementos comunes, y deseas que tus reglas los sobrescriban. Cargar tu hoja de estilo después de las suyas lo hace sencillo:
<link rel="stylesheet" href="reset.css">
<link rel="stylesheet" href="style.css">
Cuando tus selectores tienen la misma especificidad que los de la biblioteca, el archivo cargado más tarde prevalece; por lo tanto, colocar style.css después de reset.css permite que tus declaraciones tengan efecto sin aumentar la complejidad de los selectores.
Depender del orden también tiene un costo. Si alguien vuelve a solicitar los tags <link> o las importaciones en un punto de entrada del bundler, los estilos pueden cambiar silenciosamente. Donde sea posible, prefiera reglas cuya especificidad haga claro cuál es la opción deseada, y utilice el orden de origen como criterio decisivo final para los casos de empate. Las capas en cascada, donde lo permitan los navegadores objetivo, son una forma más explícita de indicar “la biblioteca primero, nuestros estilos después”.
Después del ganador: el valor en cascada
En este punto ya se ha respondido la primera pregunta. El navegador recopiló todas las declaraciones de la propiedad, evaluó su importancia, luego su especificidad y finalmente el orden de origen, para decidirse por una. Ese valor ganador se denomina valor en cascada.
No obstante, el trabajo aún no está completo. Supongamos que la declaración ganadora es:
width: 66%;
Un porcentaje como este no puede ser dibujado directamente; el navegador aún debe procesarlo, resolviéndolo en relación con el bloque contenedor para obtener una longitud real. Ese procesamiento de valores, junto con la herencia de propiedades que no tienen declaración alguna, constituye la siguiente etapa para convertir CSS en píxeles.
Puntos clave
- La cascada resuelve los conflictos en un orden fijo: importancia, luego especificidad y finalmente el orden de las fuentes.
- La especificidad se determina mediante una comparación en cuatro columnas (inline, IDs, clases/pseudo-clases/atributos, elementos/pseudo-elementos) leída de izquierda a derecha; las columnas inferiores nunca influyen en las superiores.
- Confirme que una regla realmente coincide con el elemento antes de comparar su especificidad; la parte más a la derecha de un selector es lo que se utiliza como objetivo.
:hover u otra de pseudo-clase solo agrega un peso a nivel de clase y puede ser anulada por una regla base más específica.!important gana al cambiar el nivel de importancia, no al resolver el conflicto; úselo de forma deliberada y con moderación.Lecturas relacionadas
- De dónde proceden los valores CSS cuando se setea ninguno: cómo funciona la herencia — Aprenda cómo deciden los navegadores si una propiedad se hereda, por qué los hijos reciben el valor calculado del padre, y cómo inherit e initial le brindan un control explícito.