Cascade layers
An extra sort step above specificity, and a rule about unlayered styles that turns out to be the most useful part of the feature.
Firefox has this in a release in two weeks. Chrome and Safari both have it within about six weeks of that. Three engines, one feature, essentially simultaneously. That is not how any of this has ever gone in my career, and it’s worth noting on its own.
The feature is @layer, and it adds a step to how the cascade resolves conflicts. I’ve been reading about it since the fall and building with it behind a flag over Christmas, and I think it’s the most significant thing to happen to CSS architecture since custom properties. That’s a slightly different claim from being the most significant feature.
Timing helps. I started somewhere new three weeks ago, which means I’m back to reading several hundred kilobytes of CSS I didn’t write, on a platform whose own stylesheets I don’t get to touch. There is no exercise that makes you want this feature more.
Specificity is a bad proxy for intent
Start with the problem, because the feature only makes sense as an answer to it.
When two rules set the same property on the same element, the browser picks the more specific one. And specificity is counted structurally: how many ids, classes and elements are in the selector.
Which means the cascade is resolving conflicts using a number that describes the shape of your selector, and using it as a stand-in for how deliberate you were. Those two things are correlated by accident at best.
The cascade resolves conflicts using a number that describes the shape of your selector, as a proxy for how much you meant it. Nobody designed that. It's just what was available.
So a reset rule written with a compound selector because that’s what it needed outranks a component rule written with one class. A vendor stylesheet you can’t edit outranks the styles you wrote to correct it. A utility class you wrote precisely to be an override loses to the component it was meant to override, because the component’s selector happens to have two classes in it.
And the standard responses to all of that are: add another class, add an id, add !important, or nest the selector deeper. All four make the next conflict worse. That’s an arms race with no ceiling and I’ve been in it my entire career.
An extra sort step
Layers add a sort that happens before specificity is considered at all.
You declare the order once, at the top, before anything else. Then a rule in a later layer beats a rule in an earlier layer, full stop, regardless of how the selectors are shaped.
@layer reset, base, layout, components, utilities;
@layer reset {
#page .content ul li { margin: 0; } /* wildly specific, and it loses */
}
@layer components {
li { margin-bottom: 0.5rem; } /* one element selector, and it wins */
}
Declaring the order up front, on its own line, is the move that makes this work. The layers get their priority from that statement, not from where the blocks physically appear, so you can add rules to reset from a file loaded at the bottom and it still sorts as a reset.
Specificity still applies within a layer, which is right. It stops being the thing that arbitrates between parts of your codebase that were never meant to be competing.
Unlayered styles win, and that’s the useful bit
The rule that seems backwards until you use it: anything not in a layer beats everything that is.
Which sounds like it defeats the purpose, and is actually the single most practical thing in the feature, because it means the adoption path is free.
@import url("some-framework.css") layer(vendor);
/* and then all of your existing CSS, unchanged, unlayered, wins */
One line, and a third-party stylesheet you can’t edit and have been fighting with !important for four years is now permanently subordinate to everything you write. You haven’t restructured anything. You haven’t touched your own code. The specificity arms race with that framework is simply over.
That’s the change I’d make first on any existing project, and it’s about ten minutes.
Utilities without !important
The one I’m most pleased about personally, because I wrote a slightly grudging post in 2017 about coming round to utility classes and this fixes the thing that always bothered me about them.
A utility exists to override. .mb-0 means “no bottom margin, I mean it.” But .card__footer has more specificity than .mb-0, so the utility loses, so every utility framework ever built has shipped !important on everything, horrible in itself and then making the utilities impossible to override.
Put utilities in the last layer and the problem dissolves. One class, no !important anywhere, and it beats the component because you said it should, not because you counted selectors.
The !important inversion
Worth knowing before it surprises you: !important reverses the layer order.
An important declaration in an earlier layer beats an important declaration in a later one. So !important in your reset layer outranks !important in your utilities layer: the opposite of the normal ordering.
It reads as a bug and it’s deliberate. The reasoning is that !important is meant to be an escape hatch, and the person writing the most foundational layer is the one whose escape hatch should be strongest. It’s the same logic that already made user stylesheets’ important declarations beat the author’s.
I’ve not had to rely on it. I mostly mention it so that when it happens to you at 5 pm you don’t lose an hour.
What it doesn’t do
It doesn’t fix an existing mess. It gives you a way to stop a mess getting worse at the boundaries between chunks of code, and the vendor case is genuinely a one-liner, but layering your own 4,000 lines properly means deciding what each of those lines was for, which is the same audit as the dark mode one and takes the same amount of time.
It doesn’t reduce specificity within a layer. Your component styles can still be a selector arms race with themselves.
And nested layers exist and get confusing quickly. I’d stay with a flat list of four or five for as long as possible.
An architecture feature
The thing I keep coming back to is that this is a feature about architecture rather than about appearance. It doesn’t let you draw anything you couldn’t draw before. It lets you say which parts of your codebase are allowed to overrule which, which we have all been expressing for 20 years by counting class names and shouting !important at each other.
Three browsers in six weeks, as well. Ask me in a year, but that feels like the sort of thing that happens when everyone involved has been quietly annoyed about the same problem for a decade.