Capas, especificidad y el orden real de la cascada
Cómo el panel refleja las capas de cascada, dónde se lee la especificidad, y el orden completo de criterios que decide qué declaración gana.
La cascada no es una comparación de especificidades: la especificidad es el cuarto criterio de una lista de seis, y los tres que van antes la anulan por completo. Casi todo el sufrimiento con CSS viene de razonar solo con el cuarto y no saber que los otros existen. El panel de estilos muestra los seis, pero de una forma que hay que aprender a leer, porque cada criterio se manifiesta en un sitio distinto de la interfaz.
- Enunciar los seis criterios de la cascada en su orden real de aplicación.
- Leer en el panel a qué capa pertenece cada regla y qué implica su posición.
- Consultar la especificidad de un selector sin calcularla a mano.
- Diagnosticar un conflicto de cascada identificando en qué criterio se decidió.
Los seis criterios en orden
Cuando dos declaraciones definen la misma propiedad para el mismo elemento, el motor las compara con esta secuencia. En cuanto un criterio decide, los siguientes no se consultan.
Primero, el origen y la importancia. El orden completo, de más fuerte a más débil: declaraciones de transición en curso; agente de usuario con !important; usuario con !important; autor con !important; animaciones en curso; autor normal; usuario normal; agente de usuario normal. Que las transiciones estén por encima de todo y las animaciones justo por encima del autor normal explica dos comportamientos que desconciertan: por qué una transición gana a un !important mientras dura, y por qué una animación gana a tu CSS pero pierde contra un !important de autor.
Segundo, las capas de cascada. Dentro del mismo origen, las capas se comparan por el orden en que fueron declaradas. Lo no encapsulado en ninguna capa gana a todo lo que esté en capas. Y hay un giro crucial: para las declaraciones !important, el orden de las capas se invierte. La primera capa declarada es la que gana en importantes, precisamente para que un sistema de diseño pueda blindar valores que las capas posteriores no puedan romper.
Tercero, el ámbito. Con @scope, la proximidad al elemento raíz del ámbito desempata: gana el ámbito más cercano.
Cuarto, la especificidad. El famoso trío de números, comparado lexicográficamente.
Quinto, el orden de aparición. Con todo lo anterior igualado, gana la última declarada.
Y hay un criterio anterior a todos que no está en la lista porque no es una comparación: la declaración tiene que casar. Los cinco criterios operan solo entre reglas que efectivamente aplican al elemento.
Dónde se lee cada criterio en el panel
Esta es la parte que convierte la teoría en práctica.
El origen se lee en la procedencia a la derecha de cada regla: un fichero de autor, la hoja del agente de usuario, o una hoja inyectada. La importancia se lee en el propio !important de la declaración, que el panel resalta.
Las capas aparecen como un encabezado sobre las reglas que pertenecen a ellas, con el nombre de la capa. Y el panel ofrece además una vista de capas de cascada —accesible desde el botón correspondiente en la cabecera del panel de estilos— que muestra el árbol completo de capas de la página en su orden real, incluidas las anidadas. Esa vista responde de un vistazo a la pregunta que ninguna otra herramienta responde: en qué orden están declaradas las capas de este proyecto.
El ámbito aparece como encabezado igual que las capas, indicando la regla de @scope que envuelve.
La especificidad aparece al pasar el cursor sobre el selector de una regla: el panel muestra la tripleta correspondiente. No hay que calcularla a mano ni recordar cuánto vale un atributo frente a una clase.
El orden de aparición es implícito: se lee en la posición relativa de dos reglas con todo lo demás igualado.
El anidamiento nativo de CSS y :is() tienen una regla de especificidad que sorprende: la especificidad de :is(), :not() y :has() es la del argumento más específico de su lista, mientras que la de :where() es siempre cero. Eso hace de :where() la herramienta correcta para escribir valores por defecto que cualquier regla posterior pueda sobrescribir sin pelear.
Diagnosticar un conflicto por criterios
El procedimiento que convierte “no entiendo por qué gana esa” en una respuesta en menos de un minuto. Se recorre la lista de arriba abajo y se para en el primer criterio donde las dos reglas difieran.
| Pregunta | Si difieren | Ya tienes la respuesta |
|---|---|---|
¿Alguna tiene !important? |
Sí | Gana esa, salvo que ambas la tengan |
| ¿Hay una animación o transición activa? | Sí | Gana esa mientras dure |
| ¿Están en capas distintas? | Sí | Gana la capa posterior, o la anterior si son importantes |
¿Hay un @scope implicado? |
Sí | Gana el ámbito más próximo |
| ¿Tienen especificidad distinta? | Sí | Gana la mayor |
| Si todo lo anterior empata | — | Gana la que aparezca después |
Lo importante del procedimiento no es la tabla: es la disciplina de no saltar al cuarto criterio directamente, que es lo que hace todo el mundo. La mitad de los conflictos que la gente intenta resolver subiendo la especificidad estaban decididos en el criterio uno o en el dos, y por tanto la especificidad no los va a mover ni añadiendo diez clases.
El caso de las animaciones y transiciones
Merece un párrafo propio porque produce el bug más desconcertante de esta lección. Una propiedad que está siendo animada o que está en medio de una transición tiene un valor que gana a las declaraciones normales del autor, y en el caso de las transiciones gana incluso a las importantes.
El síntoma: cambias un valor en el panel, no pasa nada, lo cambias otra vez, sigue sin pasar nada. Y en cuanto la transición termina, tu cambio aparece de golpe. O peor: la propiedad tiene una animación infinita y tu valor no se aplica nunca.
La comprobación es directa desde la consola.
// Que animaciones y transiciones estan afectando ahora mismo a este elemento
console.table(
$0.getAnimations().map(a => ({
tipo: a.constructor.name,
propiedad: a.transitionProperty ?? a.animationName ?? '',
estado: a.playState
}))
);
getAnimations devuelve tanto las animaciones de CSS como las transiciones y las creadas con la API de animaciones web, así que cubre todos los casos. Si la lista está vacía, descarta esta hipótesis; si tiene algo, ya sabes por qué tu declaración no manda.
La llegada de @layer es el cambio más profundo que ha tenido la cascada desde que existe, y su valor no es técnico sino organizativo. Antes de las capas, la única forma de decir “estos estilos deben poder sobrescribir a aquellos” era la especificidad, y la especificidad tiene un defecto estructural: es una propiedad del selector, no del rol de la regla. Para que un estilo de componente ganara a un reset había que escribir un selector más largo, lo que acoplaba la fuerza en la cascada a la forma del selector y convertía cualquier refactor de marcado en un riesgo de cascada. El resultado fue una década de proyectos con .app .card .title.title y de !important puestos por rendición. Con capas, la fuerza se declara aparte: @layer reset, base, componentes, utilidades; en una línea al principio del proyecto establece el orden completo, y a partir de ahí cada regla puede usar el selector más simple que exprese su intención, porque su prioridad no depende de él. En el panel esto se traduce en un diagnóstico mucho más limpio, porque el encabezado de capa te dice el rol de la regla que gana, y eso convierte “esta regla tiene especificidad 0,3,1” —un dato sin significado— en “esta regla es una utilidad y por eso gana a un componente” —una frase que se puede discutir en una revisión de código. Hay una trampa que hay que conocer y es la que más sorprende: el CSS sin capa gana a todo el CSS con capa, así que introducir capas en un proyecto existente sin meter también el código heredado en una capa produce el efecto contrario al esperado. El código viejo, que era el que querías poder sobrescribir, pasa a ser el más fuerte del proyecto.