Lesson 1 of 5
The Cascade, Specificity & Inheritance
"Simplicity does not precede complexity, but follows it." — Alan Perlis, Epigrams on Programming (1982)
CSS is the language people claim not to respect and cannot reliably predict. Both things are true at once, and they are related: it looks like a list of declarations, so nobody expects it to have semantics worth learning, so the semantics get learned by collision. The result is an engineer who can write a distributed queue and cannot say why their color is being ignored.
What is actually happening is a resolution algorithm. When several declarations target the same property on the same element — which is the normal case, not the exception — the browser runs a defined sequence of tie-breakers to pick one. That sequence is the cascade, and specificity is only one step inside it. Inheritance is a separate mechanism that runs after resolution, for properties where the cascade produced no answer at all.
Treating these as three rules of thumb is what produces the folk practices: !important sprinkled to win arguments, selectors lengthened until they stick, #id used for weight rather than identity. Treating them as one algorithm makes the behaviour predictable, and predictable CSS is the entire difference between a stylesheet that survives three years and one that gets rewritten.
The specific correction most engineers need is this: specificity is not a number. It is a three-part tuple compared left to right, and there is no rollover. Eleven classes do not beat one id — not because eleven is less than some threshold, but because the comparison never reaches the class column when the id column already differs. The block below demonstrates it directly, and once you have seen it, an entire category of "why isn't this working" resolves itself.
Two rules set color on the same element. One has eleven class selectors; the other has a single id. The eleven-class rule appears later in the file. It loses. Why is that not just "ids are stronger", and what does the real answer tell you about every other conflict you have argued with?
The cascade is an ordered algorithm
Specificity is step four of seven. That framing alone fixes several misconceptions.
Run to execute the tests…
The inversion in the second test is the part people are never told. !important was designed so a user could override an author — an accessibility mechanism. In author stylesheets it is a borrowed weapon.
Think of a court hierarchy. A dispute is settled at the lowest level that can decide it: if the first test separates the parties, nothing below it is ever consulted. Specificity is one court in that hierarchy, not the courthouse. When people say "just add another class", they are arguing louder in a court that already ruled — the case was decided upstairs on origin, or on layers, and no amount of extra classes reaches it. And when the columns compare left to right with no rollover, that is the same principle again: once the id column differs, the class column is not evidence, it is noise.
Specificity, computed
Run to execute the tests…
The final test is the everyday version of this. A hover state that will not apply is almost always losing to an id somewhere else in the file, and no amount of classes on the hover rule will reach it. The fix is deleting the id from the other selector, not strengthening this one — which is why id selectors are discouraged for styling. They create conflicts that cannot be resolved locally.
Play with it
Make the second selector .nav.nav.nav.nav a:hover and watch it still lose. Then change the first to :where(#header) .nav a and watch it flip instantly. That is the whole lesson in two edits.
Inheritance runs where the cascade found nothing
Run to execute the tests…
The fourth test is the one that catches people. A blanket * { color: … } reset is a declaration on every element, so it defeats inheritance everywhere — including the theming you wanted to flow down. Inheritance loses to any declaration, however weak. It is not competing on specificity; it only runs when there is nothing to compete with.
Håkon Wium Lie's original 1994 proposal was more radical than what shipped: styles from the author and the reader were to be blended by weight, so a user could declare "font-size: 24pt, 60% weight" and get a genuine negotiation between their needs and the designer's intent. The word cascading is the residue of that idea — it was about streams of preference merging, not about a priority list. Implementers found percentage blending impossible to specify unambiguously, and CSS1 replaced it with the discrete origin ordering we have, keeping !important as the one surviving trace of user sovereignty. That history explains the design that otherwise looks arbitrary: !important inverts author and user precedence because the whole feature exists so a reader can insist. Using it to win an argument with your own stylesheet is using an accessibility mechanism as a hammer, and it works — right up to the point where you need to override it and discover you spent the last move.
The rule I hold to hardest is a specificity ceiling: single classes for almost everything, no ids in selectors ever, and when a rule genuinely needs scoping, :where() so the scope costs nothing. A stylesheet where nearly every rule sits at [0,1,0] is one where source order decides conflicts — and source order is something you can see and reason about, unlike a specificity war spread across six files. The second habit is what I do when a rule will not apply: before touching the selector at all, I open devtools, find the property, and look at what is crossed out above it. The answer is in that panel every single time, and it is frequently not what I assumed — often an inline style or a third-party stylesheet, neither of which more classes would have fixed. The mistake I made for years was treating a losing rule as a signal to make it stronger, when it is almost always a signal that something else is too strong. Strengthening is local and feels productive; it also raises the floor for everyone who comes after, and a codebase where every rule was strengthened once is a codebase that now needs !important to do anything at all.
A design system team I worked with could not get their button component to accept an override. Consumers would pass a class, it would do nothing, and the escalation path had become "add !important" — which by the time I saw it appeared in about forty places across the consuming applications. The root cause was one line written two years earlier: the component's styles were scoped as #app .ds-button, because at the time someone needed it to beat a global reset and reached for the nearest tool. Every consumer class sat at [0,1,0] against the component's [1,1,0], so nothing ever worked, and each team independently discovered !important as the workaround. Removing the #app prefix and re-scoping with :where(#app) .ds-button fixed all forty cases at once, because the scope still matched and now weighed nothing. The uncomfortable part of the story is the interval: two years, dozens of engineers, all reaching for the same hack, and not one of them looked at what they were losing to. The devtools panel had the answer the whole time.
Suppose specificity did not exist and conflicts were resolved purely by source order — last rule wins, always. What would break? Component libraries mostly would not: you would import them first and your overrides would naturally come later. Utility classes would get simpler, not harder. What you would lose is the ability for a specific rule to beat a general one written later in the same file, which is real but narrower than it sounds. Now notice that this is very close to what people already do voluntarily — flat single-class conventions and cascade layers are both, in effect, ways of turning specificity off so that order can decide. That convergence is worth sitting with. The most common way to succeed with a powerful feature is to stop using most of it, and if a whole industry's best practice is "opt out of this mechanism", the interesting question is what the mechanism was for.
Common Mistakes
Reading specificity as a three-digit number. It is a tuple compared left to right; eleven classes never reach an id.
Adding classes to a rule that is losing to an id. The comparison stops at the id column. Remove the id instead.
Using ids as styling hooks. They create conflicts that cannot be resolved locally by anyone downstream.
Reaching for
!importantto win. It changes origin bucket, not specificity — and two!importantrules fall straight back to specificity anyway.Not knowing
!importantinverts author and user. It exists so a reader can insist, which is why taking it is taking a user's tool.Ignoring
:where(). Zero specificity with a full match; it is the answer to almost every "I need to scope this but stay overridable".Grouping an id into
:is()for brevity.:is()and:not()take their strongest argument, so everything in the list is silently promoted.Believing inheritance competes with the cascade. It only runs when the cascade produced nothing; any declaration beats it.
Blanket
*resets on inherited properties. A declaration on every element defeats inheritance everywhere, breaking theming in untraceable ways.Expecting box properties to inherit. Only text-ish properties do — inherited padding would nest catastrophically.
Guessing instead of opening devtools. The crossed-out declaration tells you the winner every time, and it is regularly something you did not suspect.
Interview Corner
Open your own stylesheet and compute the specificity of every selector — the model above is about twenty lines and will run over a real file. Sort by the id column and look at what comes out on top; those are your future override problems, and there are usually more than you expect. Then take the highest one and try rewriting it with :where() so it matches identically at zero weight, and check nothing moved. Next, do the diagnosis drill: find a style in your app that is being overridden, open devtools, and identify the winner from the crossed-out declaration before forming a theory. Do that five times and you will notice how often the answer is somewhere you were not looking. Finally, add up your !important declarations and, for each one, work out what it is beating — if the answer is your own stylesheet, that is a specificity problem wearing a disguise.
Run to execute the tests…
Summary
- The cascade is an ordered algorithm, and specificity is one step inside it — after origin, layers and inline styles.
- Origin order is UA → user → author, and
!importantreverses it. It exists so a reader can insist over a designer. !importantchanges origin bucket, not specificity. Two important rules fall straight back to specificity, which is why the arms race cannot be won.- Specificity is a tuple, not a number. Compared left to right, no rollover:
[0,11,0]loses to[1,0,0]because the id column decides it first. *and combinators weigh nothing. Attribute selectors and pseudo-classes weigh the same as a class.:where()matches fully at zero weight — the tool for scoping something that must stay overridable.:is()and:not()take their strongest argument, so grouping an id into a list silently promotes everything in it.- A rule losing to an id cannot be fixed locally. Remove the id or wrap it in
:where(). - Inheritance is a fallback, not a competitor. It runs only where the cascade produced nothing, and any declaration beats it.
- Only text-ish properties inherit; box properties do not, because inherited padding would nest catastrophically.
- A blanket
*reset defeats inheritance everywhere, because it is a real declaration on every element. inherit,initial,unsetandrevertgive you explicit control;all: unsetis the cleanest way to strip UA styling.- Open devtools before theorising. The crossed-out declaration names the winner every time.
In the next chapter, The Box Model, we look at what a rule actually produces once it has won: how a box is sized, why box-sizing changed the default everyone uses, and where margins collapse into each other for reasons that are entirely deliberate.
Writing this chapter, the thing I kept circling is that almost every best practice in CSS architecture — flat single classes, no ids, BEM, utility frameworks, cascade layers, :where() — is a strategy for using less of the cascade. That is unusual. We do not normally respond to a powerful feature by building conventions to avoid it. My reading is that the cascade was designed for a web of documents where an author, a reader and a browser each had legitimate claims on the rendering, and it is genuinely well designed for that. What it was not designed for is a hundred engineers editing one stylesheet across five years, where any mechanism allowing a distant file to override a local decision is a liability rather than a feature. So the conventions are not fixing a flaw; they are adapting a document language to a software-engineering problem it never anticipated. I find that a more useful frame than "CSS is bad", partly because it is fairer and mostly because it tells you what to do: prefer the parts of the cascade that are visible and local, and opt out of the parts that act at a distance.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.