Also available in Español
The Problem
A team ships clean CSS.
Every selector is deliberate. Every class name means something. Code review catches the sloppy stuff before it merges.
Six months later, someone adds !important to fix a button.
A year later, three more !importants exist — each one written to fix the last one. Nobody planned this. Nobody stopped caring. The team is exactly as disciplined as it was on day one.
The codebase didn't get sloppy. The architecture never had a way to stay clean.
That's the part worth sitting with. Specificity problems get treated as a discipline failure — bad naming, careless nesting, someone in a hurry. But teams with excellent discipline hit this wall too. Given enough time and enough contributors, almost every CSS codebase drifts toward the same place: overrides stacked on overrides, each one a patch for the last.
Something structural is happening here. Not a people problem. A tooling gap.
Why the Problem Exists
CSS specificity was built to answer one narrow question: if two rules target the same element, which one wins?
The browser calculates an answer. Count the IDs. Count the classes and attributes. Count the elements. Higher count wins. If the count ties, whichever rule appears later in source order wins.
That's the entire mechanism. It's fast, deterministic, and was never meant to do more than that.
Notice what it doesn't ask. It doesn't ask whether a rule is a foundational default or a one-off exception. It doesn't ask whether a rule was written to be overridden, or written to never be touched again. It doesn't know the difference between a base style and a utility class — it only knows how many selectors each one used.
Specificity resolves conflicts. It was never given a way to encode intent.
That gap is old. It predates component-based frontend architecture, design systems, and teams of a hundred engineers touching the same stylesheet. The web platform gave developers a scoring system for which rule wins by the numbers — and then the industry kept building larger and larger systems on top of a mechanism that has no concept of which rule should win by design.
The trade-off is real, and worth naming plainly: specificity gives the browser something valuable — a fast, predictable way to resolve any conflict without asking the author anything. What it costs is equally real. Engineers get no built-in way to say "this always wins" or "this always loses." They have to simulate that with selector weight, and selector weight was never built to carry meaning.
The First Principle
Here's the calculation, made concrete.
.card .title scores three points — two classes, one element context beyond that isn't counted the same way, but the shorthand holds: two classes outweighs one element. h2 scores one point. It's a single element selector.
So .card .title beats h2. Always. Regardless of which rule was written first. Regardless of which one was meant to be the fallback and which was meant to be the override. The browser doesn't know intent. It only knows the count.
Now extend that. A utility class meant to override everything — .text-center, say — carries the same weight as any other single class. It can lose to .card .title even though its entire purpose was to win. The author's intent and the selector's weight have quietly diverged, and nothing in the system will ever tell them that happened. The page will just render wrong, and someone will go looking for why.
This is the principle worth naming directly: specificity is a calculation, not a decision. The cascade has no concept of "this layer should always win because it's a utility" or "this should always lose because it's a foundational default." Those categories don't exist yet, as far as the browser is concerned. All it has is arithmetic.
Demonstrating the Principle
Take a simple base style:
.card .title {
font-size: 1.25rem;
color: #1a1a1a;
}
And a component override, written later, meant to win:
h2 {
font-size: 2rem;
}
The h2 rule loses. Not because it's wrong — because it's a single element selector fighting a two-class selector, and the count always decides. The fix, tactically, looks like this:
.card h2.title {
font-size: 2rem;
}
More specific. It wins now. It also just made the next override harder, because the next developer who needs to touch this element has to beat this selector, which means writing something even heavier. Six months from now, someone adds !important to override that. The pattern repeats, each fix slightly more forceful than the last, until the stylesheet is a stack of increasingly specific patches with no record of which one is actually meant to be the source of truth.
Nobody chose this outcome. Every individual fix was reasonable. That's what makes it worth pausing on.
Where This Is Going
There is a real fix for this — and it isn't writing more careful selectors, or agreeing as a team to never use IDs, or banning !important in a linting rule. Those are all attempts to manage the symptom with more discipline, and discipline alone doesn't scale any better than specificity does.
The actual fix is architectural. It means giving the cascade something it was never given by default: an explicit ordering axis, independent of specificity, where precedence is declared instead of calculated, inside a system of contracts the whole team can see.
That mechanism exists. It's native to the browser, ships with no build step — and it's worth its own piece, later in this series.
The Broader Lesson
Step back from CSS for a moment.
Any system that lets outcomes be decided by an arithmetic side-effect — instead of a declared, intentional contract — will drift over time. It doesn't matter how careful the people using it are. Given enough contributors and enough time, implicit systems accumulate implicit knowledge: tribal rules about which files to touch, which selectors are "safe," which ones nobody dares to change. That knowledge lives in people's heads, not in the system, and it evaporates the moment those people leave.
CSS specificity is one instance of a pattern that shows up everywhere in software: precedence without a mechanism for expressing intent is precedence that eventually becomes unpredictable. The browser gave developers a conflict-resolution tool and called it done. It took years before the platform gave authors something closer to what they actually needed — and longer still before most teams noticed that architecture, not discipline, was the missing piece all along.
Top comments (0)