Stack Patterns ■ Episode 3
“The cascade, after 25 years, finally makes sense. Terribly sorry it took so long.”
For a quarter of a century, CSS specificity was a medieval
succession dispute. The throne of “which style applies”
was contested not by merit but by lineage: the depth of
your selector chain, the number of classes in your ancestry, the
presence of an #id somewhere in the bloodline. When
two rules disagreed, the one with more heraldic weight prevailed.
When both had equal claim, source order arbitrated like a tired
magistrate. And when all else failed, the developer reached for
!important, the nuclear option, the
constitutional crisis, the divine right of kings invoked by
someone who had simply run out of patience.
The war is over.
@layer
ended it. Not with a compromise, not with a polyfill, but with a
single declaration that rewrites the rules of precedence entirely.
@layer general, extern, components, themes;
Four layers. Declaration order decides priority. Last wins. That is the entire peace treaty.
The Old Regime
To appreciate what @layer resolves, one must first
recall what it replaces. The
CSS cascade
has always followed a hierarchy: origin (user-agent, author,
user), then specificity, then source order. In theory, this was
elegant. In practice, it was a siege engine aimed at your own
codebase.
You wrote a clean .button component. Then a
third-party library shipped a .btn rule with higher
specificity. Your component lost. You increased your specificity.
Then another library arrived, and its selector was deeper still.
You added !important. Then the third-party vendor
added !important. Now you were trapped in a mutual
escalation with the nuclear codes on both sides, and the only
winner was the developer who loaded their stylesheet last.
The entire discipline of CSS architecture (BEM, SMACSS, ITCSS, utility-first) was, at its core, a series of treaties attempting to prevent specificity conflicts through naming conventions and file ordering. Diplomatic solutions to a constitutional deficiency. The language lacked a way to declare: “This layer of styles outranks that layer, regardless of selector weight.”
Now it has one.
The Four Layers
The declaration is a single line. The architecture it imposes is permanent:
@layer general, extern, components, themes;
general: your reset, your CSS custom properties, your base typography. The foundation. Everything here is meant to be overridden. It sets defaults with the quiet confidence of a butler who knows the household will ignore his suggestions but makes them anyway.
extern: third-party CSS. Tailwind.
FontAwesome. That analytics widget your marketing department
insists upon. Everything you did not write and cannot fully trust.
Wrapping it in extern means it can never outrank
your own components, regardless of how many selectors the vendor
chains together.
components: your styles. Your buttons,
your cards, your layout primitives. This is where the application
lives. It automatically outranks extern because it
is declared after it. Third-party CSS wreaking havoc? Not any
more. Components win. Automatically.
themes: dark mode, high contrast, visual
variations. The final word. A single-class selector in
themes outranks a five-selector chain in
components. Not because its specificity is higher
(it almost certainly is not) but because layer
order outranks specificity entirely. The succession is settled
by constitutional law, not by the size of the army.
The Precedence Revolution
This is the part that rewrites two decades of muscle memory.
Under the old regime, specificity was the primary arbiter. A
selector with (1,2,0) beat a selector with
(0,3,0), regardless of where each appeared.
@layer inverts that hierarchy. Layer order is
evaluated before specificity. Specificity only matters
within the same layer.
Let that settle for a moment. Five selectors deep in
components loses to one selector in
themes. Not because of a hack. Not because of
!important. Because the
W3C CSS Cascade Level 5 specification
says so. The cascade finally has a proper constitution, and
specificity has been demoted from sovereign to civil servant.
Sublayers: Divide the Vassal States
Layers nest. The extern layer can contain its own
internal hierarchy, each third-party dependency isolated from the
others:
@layer extern {
@layer tailwind, fontawesome, other;
}
@layer extern.tailwind {
@import url("tailwind.css");
}
@layer extern.fontawesome {
@import url("fontawesome.css");
}
Tailwind and FontAwesome now occupy separate sublayers within
extern. They cannot outrank each other by accident.
They cannot outrank your components at all. The feudal metaphor
holds: vassals may squabble amongst themselves, but they do not
challenge the crown.
The @import syntax integrates directly with layer
declarations. No wrapper needed, no build step required. The
browser understands the assignment.
The Unlayered Exception
There is one important caveat, and it is the kind that will catch you precisely once before you remember it for life. Unlayered styles (any CSS not assigned to a layer) override everything. They sit above the layer stack entirely, as if the constitution does not apply to them.
/* This unlayered rule beats ALL layers */
.debug-border {
outline: 2px solid red;
}
For debugging, this is useful. A quick diagnostic rule that
overrides everything without touching the layer declarations.
For production, it is dangerous. An unlayered stylesheet
imported without a layer() qualifier will silently
override your entire architecture. The constitutional monarchy
works only if everyone is inside the constitution.
Browser Support: Already Shipped
Every modern browser
has supported @layer since March 2022. Chrome 99.
Firefox 97. Safari 15.4. Edge 99. There is no polyfill required.
There is no build step. There is no “we shall adopt it
once the ecosystem matures” excuse remaining. The
ecosystem matured four years ago. The only thing missing is
your adoption.
The Peace Treaty
The specificity war lasted twenty-five years. Developers fought it with naming conventions (BEM), with methodologies (ITCSS), with utility classes (Tailwind), and with the outright rejection of CSS in favour of JavaScript (CSS-in-JS). Each approach was a workaround for the same constitutional deficiency: the cascade had no mechanism for declaring layer precedence.
Now it does. One line. Four layers. Declaration order decides priority. Layer order outranks specificity. The diplomatic workarounds were honourable. They are no longer necessary.
!important is no longer a weapon. It is an
archaeological artefact.
The cascade, after 25 years, finally makes sense. Terribly sorry it took so long.