Cascada moderna
Lección 23 de 37 · 5 min
Cuando dos reglas le ponen un valor distinto a la misma propiedad, la cascada decide cuál gana. Con capas, nesting y @scope podés ordenar tus estilos sin subir la especificidad a la fuerza.
El orden de la cascada
El navegador junta todas las declaraciones que aplican a un elemento y las compara por pasos. Solo pasa al siguiente si hay empate:
- Origen e importancia
!importantTus estilos le ganan a los del navegador, y una declaración con!importantle gana a una normal. - Contexto
shadow DOMCon Shadow DOM, los estilos del componente y los de la página se comparan acá. - Atributo style
style="…"Lo escrito en el atributo le gana a cualquier selector. - Capas
@layerGana la capa declarada después, y lo que no está en ninguna capa le gana a todas. - Especificidad
(1, 0, 1)La que se explica en Selectores y cascada. - Proximidad
@scopeEntre dos reglas de@scope, gana la de la raíz más cercana. - Orden de apariciónA igualdad de todo lo anterior, gana la última.
Fijate dónde está la especificidad: después de las capas. Una regla con un ID en una capa temprana pierde contra una clase en una capa posterior. Eso es lo que hace útiles a las capas.
Capas con @layer
Una línea con @layer y una lista de nombres fija el orden de las capas: la primera es la más débil. Después cada bloque @layer nombre { … } mete reglas en su capa, en cualquier orden y en cualquier archivo.
@layer reset, base, componentes;@layer componentes {.boton { background: royalblue; }}@layer base {button { background: gray; } /* pierde: base va antes */}/* también se puede importar un archivo entero en una capa */@import url("vendor.css") layer(vendor);
El orden lo decide la primera vez que aparece cada nombre. Por eso conviene declarar la lista arriba de todo: si un bloque aparece antes que la lista, su capa queda primera.
Cambiá el orden, sumá una regla sin capa y probá el !important. El panel muestra por qué pierde cada regla:
El enlace “Ver combinaciones” se pinta de azul: gana la regla de @layer componentes.
Línea B · Aviso de servicio
Hasta el 30 de noviembre, los trenes no paran en Pasteur–AMIA por obras en los andenes.
Estilos de a.enlace
- @layer baseespecificidad (1, 0, 1)
#aviso acolor: var(--gris) - @layer componentesespecificidad (0, 1, 0)
.enlacecolor: var(--azul)
@layer base, componentes;@layer base {#aviso a {color: var(--gris);}}@layer componentes {.enlace {color: var(--azul);}}
Un @layer { … } sin nombre crea una capa anónima: ocupa su lugar en el orden, pero no se le pueden sumar reglas después. Las capas también se anidan: @layer framework { @layer base { … } } crea framework.base, y adentro de framework sus subcapas se ordenan igual que afuera.
Las capas son Baseline desde marzo de 2022 (Chrome y Edge 99, Firefox 97, Safari 15.4) y ampliamente disponibles desde septiembre de 2024.
!important da vuelta las capas
Con !important el orden de las capas se invierte: gana la capa declarada antes, y lo que no tiene capa queda último. Parece raro, pero tiene sentido: una capa de reset puede marcar algo como importante y ninguna capa posterior lo pisa sin querer.
@layer reset, componentes;@layer reset {[hidden] { display: none !important; } /* gana: reset va antes */}@layer componentes {.menu { display: flex !important; }}
Lo mismo pasa con los orígenes: el !important del navegador le gana al tuyo. Como regla práctica, ordená con capas y dejá !important para casos así, donde algo nunca debería pisarse.
Nesting nativo
CSS ya permite escribir reglas adentro de otras, como en Sass. & representa al selector de afuera; si no lo ponés, la regla anidada se lee como descendiente.
.tarjeta {padding: 16px;h2 { font-size: 18px; } /* .tarjeta h2 */&:hover { background: #f5f5f5; } /* .tarjeta:hover */.oscuro & { color: white; } /* .oscuro .tarjeta */@media (width >= 40rem) {padding: 24px; /* .tarjeta, en pantallas anchas */}}
Dos diferencias con Sass. No concatena texto: &__titulo no genera .tarjeta__titulo. Y el selector de afuera se trata como si estuviera dentro de :is(), así que toma la especificidad del más alto de la lista:
#menu, .menu {a { color: inherit; }}/* es :is(#menu, .menu) a → (1, 0, 1), también para .menu a */
El nesting es Baseline desde diciembre de 2023 (Chrome y Edge 120, Firefox 117, Safari 17.2) y ampliamente disponible desde junio de 2026. Chrome 112 a 119 ya lo soportaba, pero sin reglas anidadas que empiecen con una etiqueta, como h2 sin &.
@scope
@scope limita reglas a una parte del documento. La raíz marca dónde empieza y el límite, opcional, dónde termina: lo que está dentro del límite (y el límite mismo) queda afuera.
@scope (.receta) to (.nota) {:scope { border: 1px solid; } /* la raíz misma */p, li { font-family: Georgia, serif; }}
Desmarcá el límite: la nota del editor también pasa a la tipografía con serifa.
Cocina · Recetas al horno
Fainá
Masa fina de harina de garbanzos, al horno bien fuerte hasta que el borde se dore.
- 250 g de harina de garbanzos
- 750 ml de agua
- 4 cucharadas de aceite de oliva
- Sal y pimienta negra
Nota del editor: en Génova se llama farinata y se come sola, no arriba de la pizza.
@scope (.receta) to (.nota) {p, li {font-family: Georgia, serif;}}
La raíz no suma especificidad: p adentro de @scope (.receta) sigue valiendo (0, 0, 1). :scope cuenta como una pseudoclase, (0, 1, 0).
Cuando dos scopes chocan con la misma especificidad, gana el de la raíz más cercana al elemento, aunque esté antes en el código. Eso resuelve los temas anidados:
@scope (.claro) { a { color: navy; } }@scope (.oscuro) { a { color: skyblue; } }/* <div class="oscuro"> … <div class="claro"> <a> …el enlace es navy: .claro está más cerca */
Un <style> con @scope sin raíz se aplica al elemento que lo contiene. @scope está en Chrome y Edge desde la 118, Safari desde la 17.4 y Firefox recién desde la 146: es Baseline desde marzo de 2026. Si tenés que soportar navegadores más viejos, las reglas adentro de un @scope que el navegador no entiende se ignoran enteras.
En Tailwind
Tailwind v4 ya usa capas: declara @layer theme, base, components, utilities. Por eso una clase utilitaria le gana a tu CSS de components aunque tenga menos especificidad, y tu CSS sin capa le gana a todo Tailwind.
| CSS | Tailwind |
|---|---|
| estilos de base o componentes | @layer components { … } |
| una utilidad propia | @utility nombre { … } |
| color: … !important | text-red-600! |
| .clase a { … } | [&_a]:underline |
| hijos · descendientes | *:text-sm · **:text-sm |
| @scope | [&_p:not(.nota_*)]:font-serif |
@import "tailwindcss";@layer components {.aviso a { color: var(--color-blue-700); }}@utility enlace-sutil {color: inherit;text-decoration-color: var(--color-neutral-300);}
<!-- utilities va después de components: gana text-green-700 --><p class="aviso">… <a class="text-green-700 hover:enlace-sutil" href="/combinaciones">Ver combinaciones</a></p>
The modern cascade
Lesson 23 of 37 · 4 min
When two rules give the same property different values, the cascade picks the winner. Layers, nesting and @scope let you organize styles without pumping up specificity.
The cascade order
The browser gathers every declaration that applies to an element and compares them step by step. It only moves to the next step on a tie:
- Origin and importance
!importantYour styles beat the browser's, and an!importantdeclaration beats a normal one. - Context
shadow DOMWith Shadow DOM, component styles and page styles are compared here. - The style attribute
style="…"Whatever is in the attribute beats any selector. - Layers
@layerThe layer declared later wins, and anything outside a layer beats all of them. - Specificity
(1, 0, 1)As explained in Selectors and the cascade. - Proximity
@scopeBetween two@scoperules, the one with the closer root wins. - Order of appearanceIf everything above ties, the last one wins.
Notice where specificity sits: after layers. A rule with an ID in an early layer loses to a single class in a later one. That's what makes layers useful.
Layers with @layer
An @layer statement with a list of names sets the layer order: the first one is the weakest. Then each @layer name { … } block adds rules to its layer, in any order and from any file.
@layer reset, base, components;@layer components {.button { background: royalblue; }}@layer base {button { background: gray; } /* loses: base comes first */}/* a whole file can be imported into a layer too */@import url("vendor.css") layer(vendor);
The order comes from the first time each name shows up. That's why the list goes at the very top: if a block appears before it, its layer goes first.
Swap the order, add an unlayered rule and try !important. The panel says why each rule loses:
The “See connections” link is painted blue: the rule in @layer components wins.
Line B · Service notice
Until November 30, trains won’t stop at Pasteur–AMIA while the platforms are rebuilt.
Styles for a.link
- @layer basespecificity (1, 0, 1)
#notice acolor: var(--gray) - @layer componentsspecificity (0, 1, 0)
.linkcolor: var(--blue)
@layer base, components;@layer base {#notice a {color: var(--gray);}}@layer components {.link {color: var(--blue);}}
An unnamed @layer { … } creates an anonymous layer: it takes its place in the order, but nothing can be added to it later. Layers also nest: @layer framework { @layer base { … } } creates framework.base, and inside framework its sublayers are ordered the same way as outside.
Layers have been Baseline since March 2022 (Chrome and Edge 99, Firefox 97, Safari 15.4) and widely available since September 2024.
!important flips the layers
With !important the layer order is reversed: the layer declared earlier wins, and unlayered styles come last. It sounds odd, but it's on purpose: a reset layer can mark something as important and no later layer overrides it by accident.
@layer reset, components;@layer reset {[hidden] { display: none !important; } /* wins: reset comes first */}@layer components {.menu { display: flex !important; }}
Origins work the same way: the browser's !important beats yours. In practice, organize with layers and keep !important for cases like this one, where something should never be overridden.
Native nesting
CSS can now nest rules inside each other, like Sass. & stands for the outer selector; without it, the nested rule reads as a descendant.
.card {padding: 16px;h2 { font-size: 18px; } /* .card h2 */&:hover { background: #f5f5f5; } /* .card:hover */.dark & { color: white; } /* .dark .card */@media (width >= 40rem) {padding: 24px; /* .card, on wide screens */}}
Two differences from Sass. It doesn't join strings: &__title doesn't produce .card__title. And the outer selector behaves as if wrapped in :is(), so it takes the specificity of the strongest selector in the list:
#menu, .menu {a { color: inherit; }}/* means :is(#menu, .menu) a → (1, 0, 1), for .menu a too */
Nesting has been Baseline since December 2023 (Chrome and Edge 120, Firefox 117, Safari 17.2) and widely available since June 2026. Chrome 112 to 119 already had it, but not for nested rules starting with a tag, like h2 without &.
@scope
@scope limits rules to one part of the document. The root says where it starts and the optional limit where it ends: whatever is inside the limit (and the limit itself) is left out.
@scope (.recipe) to (.note) {:scope { border: 1px solid; } /* the root itself */p, li { font-family: Georgia, serif; }}
Uncheck the limit: the editor's note switches to the serif font too.
Cooking · Oven recipes
Fainá
A thin chickpea-flour flatbread, baked in a very hot oven until the edge turns golden.
- 250 g chickpea flour
- 750 ml water
- 4 tablespoons olive oil
- Salt and black pepper
Editor’s note: in Genoa it’s called farinata and eaten on its own, not on top of pizza.
@scope (.recipe) to (.note) {p, li {font-family: Georgia, serif;}}
The root adds no specificity: p inside @scope (.recipe) is still (0, 0, 1). :scope counts as a pseudo-class, (0, 1, 0).
When two scopes clash with equal specificity, the one whose root is closer to the element wins, even if it comes first in the code. That solves nested themes:
@scope (.light) { a { color: navy; } }@scope (.dark) { a { color: skyblue; } }/* <div class="dark"> … <div class="light"> <a> …the link is navy: .light is closer */
A <style> with a root-less @scope applies to the element that contains it. @scope shipped in Chrome and Edge 118, Safari 17.4 and only in Firefox 146: it's been Baseline since March 2026. If you support older browsers, rules inside an @scope the browser doesn't understand are dropped entirely.
In Tailwind
Tailwind v4 already uses layers: it declares @layer theme, base, components, utilities. That's why a utility class beats your CSS in components even with lower specificity, and your unlayered CSS beats all of Tailwind.
| CSS | Tailwind |
|---|---|
| base or component styles | @layer components { … } |
| your own utility | @utility name { … } |
| color: … !important | text-red-600! |
| .class a { … } | [&_a]:underline |
| children · descendants | *:text-sm · **:text-sm |
| @scope | [&_p:not(.note_*)]:font-serif |
@import "tailwindcss";@layer components {.notice a { color: var(--color-blue-700); }}@utility quiet-link {color: inherit;text-decoration-color: var(--color-neutral-300);}
<!-- utilities comes after components: text-green-700 wins --><p class="notice">… <a class="text-green-700 hover:quiet-link" href="/connections">See connections</a></p>