Vivian Voss

The Specificity War Is Over

css web architecture

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 Old Regime Selector weight decides .button 0,1,0 nav .btn.primary 0,3,0 #app .nav .btn 1,2,0 .button { !important } Escalation spiral @layer Declaration order decides general lowest extern components themes highest Last declared wins. Always. Five selectors deep in components loses to one selector in themes. Layer order outranks specificity.

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.

Specificity vs Layer Order components #app main .card .body p Specificity: 1,2,1 LOSES vs themes .dark p Specificity: 0,1,1 WINS Layer order is evaluated before specificity. The smaller bar wins because themes outranks components.

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.

Layer Hierarchy with Sublayers themes components extern tailwind fontawesome other Sublayers resolve internally, never outrank parent general | | | priority

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.