Appearance
6.2.2 — The Cascade, Specificity & Inheritance
Two rules, both targeting the same button, and the one written last loses:
css
/* Written first, in a file loaded first. This one wins. */
#checkout .btn { background: navy; }
/* Written last, in the file loaded last. This one loses. */
.btn.btn-danger { background: crimson; }The button is navy. Nothing is broken, no rule was ignored, and adding !important to make it crimson is the wrong fix that a hundred thousand codebases have shipped anyway.
The C in CSS stands for cascading, and the cascade is a specified algorithm with defined steps. Once you can run it in your head, the "CSS is unpredictable" feeling disappears completely — the language is, in fact, one of the most rigidly deterministic things in frontend.
1. The cascade, in the order it actually runs
When several declarations set the same property on the same element, the browser sorts them by a fixed sequence of tie-breakers. It compares the first criterion; only if that ties does it look at the second, and so on.
Step 1: origin and importance
Three parties can supply CSS. The user agent — the browser's own default stylesheet, the thing that makes <h1> big and links blue. The user — settings and extensions, such as a forced minimum font size. The author — you.
Normal declarations rank: user agent, then user, then author. Your CSS beats the browser's defaults, which is what you expect.
Then !important reverses the order, and the reversal is the interesting part. An author !important beats a normal author rule, but a user !important beats your !important. That is deliberate accessibility design: a user who needs 24-pixel text must be able to override a site that hard-codes 11 pixels, and no amount of author !important can take that away from them.
CSS transitions sit above everything, including !important, and animations sit just below !important declarations. This is why a running transition appears to ignore a rule you just set — for the duration of the transition, it does.
Step 2: cascade layers
@layer is the newest piece of the cascade and the most useful one to know, because it solves the problem in the opening example properly.
css
/* (1) Declare the order once, at the top. Later layers beat earlier ones. */
@layer reset, framework, components, utilities;
@layer framework {
/* (2) High specificity, but it is in an early layer. */
#checkout .btn { background: navy; }
}
@layer components {
/* (3) Low specificity — and it wins anyway. */
.btn.btn-danger { background: crimson; }
}Line (1) fixes the layer order for the whole document, regardless of where the layers are later filled in. Line (2) has specificity of one id plus one class, which under normal rules is unbeatable by line (3). But layer order is checked before specificity, and components comes after framework, so line (3) wins.
This inverts the usual advice. For twenty years the answer to "a third-party stylesheet is beating my override" was to write a more specific selector, which started a specificity arms race that ended in !important everywhere. With layers you put the third-party CSS in an early layer and stop thinking about its selectors entirely.
Two details that matter in practice. Unlayered CSS beats all layered CSS, so ordinary styles written outside any @layer block still sit on top; the mental model is that unlayered is an implicit final layer. And !important inverts layer order too, exactly as it inverts origin order — an !important in the earliest layer beats an !important in the latest one. That is consistent once you see the pattern, and surprising the first time it bites.
Step 3: specificity
If two declarations survive to here, the browser scores each selector as three numbers, conventionally written (a, b, c):
- a — the number of id selectors (
#header) - b — the number of class selectors, attribute selectors and pseudo-classes (
.card,[disabled],:hover,:nth-child()) - c — the number of type selectors and pseudo-elements (
div,a,::before)
Compare left to right. Any a beats every b, no matter how many. Worked examples:
| Selector | a | b | c | Read as |
|---|---|---|---|---|
* | 0 | 0 | 0 | nothing |
li | 0 | 0 | 1 | one type |
ul li a | 0 | 0 | 3 | three types |
.nav a | 0 | 1 | 1 | a class beats any number of types |
a:hover | 0 | 1 | 1 | pseudo-class counts as a class |
.nav .item.active | 0 | 3 | 0 | three classes |
#main .nav a | 1 | 1 | 1 | the id decides it |
#a #b | 2 | 0 | 0 | two ids |
Three things sit outside the scoring and are worth knowing exactly:
An inline style attribute beats every selector, at any specificity. Think of it as a fourth number in front.
!important is not part of specificity at all — it is step 1. Specificity is only used to break ties between important declarations or between normal ones.
The pseudo-classes that change the arithmetic:
css
/* :is() and :not() take the specificity of their MOST specific argument. */
:is(#sidebar, .panel) .title { } /* (1,1,0) — the #sidebar decides it */
p:not(.intro) { } /* (0,1,1) — the .intro counts */
/* :where() always scores ZERO, whatever is inside it. */
:where(#sidebar, .panel) .title { } /* (0,1,0) — only .title counts */:where() is the tool for anyone writing CSS other people will override. A design-system base style wrapped in :where() can be overridden by a single plain class, which is exactly the behaviour a library should have. It removes the need for consumers to escalate.
Step 4: document order
Everything tied, so the last one in the document wins. That is all this step is, and it is the only part most people internalise — which is why the opening example felt wrong. Order is the last tie-breaker, not the first.
Running the opening example through the algorithm
#checkout .btn scores (1,1,0). .btn.btn-danger scores (0,2,0). Both are author-origin, both normal, no layers involved, so the comparison reaches step 3, and one id outranks any number of classes. Navy wins, and no amount of moving the file later in the build changes it.
Three real fixes, best first. Put the two rule sets in cascade layers and order the layers. Stop using ids for styling — reserve them for fragment links and label for, so a stays at zero everywhere. Match the specificity you need to beat by adding one class, .btn.btn-danger.btn-danger, which is ugly but honest. And the non-fix: !important, which works today and guarantees that the next person needs a bigger hammer.
2. Inheritance: the other way a property gets a value
The cascade decides between competing declarations. Inheritance decides what happens when there are none.
Some properties inherit by default, and they are almost exactly the ones about text: color, font-family, font-size, font-weight, line-height, letter-spacing, text-align, text-transform, white-space, visibility, cursor, list-style, and the direction properties.
Most others do not: background, border, padding, margin, width, height, display, position, overflow, float, box-shadow.
The split is not arbitrary. Text properties inherit because that is what you want a hundred times out of a hundred — you set a font on <body> and expect the whole document to use it. Box properties do not inherit because inherited padding would be absurd: every nested <div> would add another 16 pixels.
The five keywords that control it explicitly
Every property accepts these, whether or not it inherits by default:
css
.child {
color: inherit; /* (1) take the parent's computed value */
border: initial; /* (2) the property's spec-defined default */
padding: unset; /* (3) inherit if inheritable, else initial */
margin: revert; /* (4) go back to the browser's default stylesheet */
font-size: revert-layer; /* (5) go back to the previous cascade layer */
}Line (1) forces inheritance where it would not happen. This is the standard trick for making a <button> or an <input> use the page's font, since form controls come with their own from the operating system:
css
button, input, select, textarea { font: inherit; }Line (2) resets to the specification's initial value, which is frequently not what the browser shows. display: initial is inline, not block, because the initial value of display in the specification is inline and the browser's default stylesheet is what makes a <div> block.
Line (3) is the general-purpose reset: inheritable properties inherit, others go initial.
Line (4) is usually the one you actually want. revert undoes your CSS and lets the browser's own stylesheet apply, so display: revert on a <div> gives you block back. The distinction between initial and revert catches nearly everybody once.
Line (5) reverts to whatever the previous cascade layer said, which is the escape hatch for layered architectures.
all applies any of them to every property at once. all: revert on a component root is a blunt but effective way to escape an inherited mess you did not write.
Where inheritance quietly bites
line-height with a unit is inherited as a computed length. body { line-height: 24px } gives a 40-pixel heading a 24-pixel line height, which overlaps. line-height: 1.5 — unitless — is inherited as the number and each element multiplies by its own font size. Always use the unitless form on shared ancestors.
Percentages resolve before inheriting. font-size: 80% on a parent and 80% again on a child gives 64%, compounding down the tree. Nested lists with a percentage font size shrink into invisibility for this reason.
visibility: hidden inherits, and a child can override it back to visible. display: none does not inherit and cannot be undone by a child, because the child is not generating a box at all. That difference is occasionally exactly what you need.
3. From declaration to pixel: the value stages
When something computes to a value you did not expect, it helps to know that a property's value passes through named stages. You do not need to recite them, but you need the two that explain real bugs.
The computed value is what the cascade and inheritance produce, with relative units mostly resolved: em becomes pixels, keywords become their real values. This is what a child inherits, and it is what getComputedStyle() reports for most properties.
The used value comes later, during layout, when things that depend on geometry are resolved: a width: 50% becomes an actual pixel count only once the parent's width is known.
The distinction explains a familiar surprise:
js
// The element is display:none, so layout never ran for it.
getComputedStyle(el).width; // → "auto", not a pixel valueThere is no used value because there was no layout. This is also, from the other direction, why reading getComputedStyle() on a visible element forces layout (Chapter 6.1.2) — the browser has to produce a used value to answer.
4. How teams keep the cascade under control
Every convention that has lasted is really a strategy for keeping specificity flat and predictable, and it is worth seeing them as answers to the same question.
BEM (block__element--modifier) gives every rule exactly one class, so every selector scores (0,1,0) and document order decides everything. Verbose, and it works. The name comes from Yandex, where it was invented.
Utility-first (class="flex gap-4 p-2") also gives everything one class, but moves the composition into the HTML instead of the stylesheet. The trade is honest: you lose the vocabulary of named components and gain the guarantee that no rule can surprise a rule somewhere else, because there are barely any rules.
Scoped styles, as produced by component frameworks, attach a generated attribute to every element and selector, so .title becomes .title[data-v-7f3a]. This buys isolation at the cost of one extra point of specificity everywhere, which is uniform and therefore harmless.
Shadow DOM is the strongest version: styles inside a shadow root genuinely cannot leak out, and outside selectors genuinely cannot reach in. It is a real boundary rather than a naming convention. Chapter 6.8.1 covers it with design systems, where the isolation is worth its cost.
Cascade layers cut across all of these, and they are the thing to reach for when you have inherited a codebase with a specificity problem rather than the freedom to rewrite it. Wrap the legacy stylesheet in an early layer and your new CSS wins without a single selector change.
What the interviewer will push on
"How does the browser decide between two conflicting rules?" Origin and importance, then layer, then specificity, then document order — and each step is only reached if the previous tied. Most candidates give specificity alone. Naming the order, and noting that document order is last, is the tell.
"Calculate the specificity of #nav .list li a:hover." One id, two class-level things (.list, :hover), two types (li, a) — (1,2,2). Then add that an inline style beats it and !important is not part of the calculation at all but a step above it.
"How do you override a third-party stylesheet you cannot edit?" Best answer: put it in an earlier cascade layer. Otherwise match its specificity deliberately, or scope your component. !important is the answer that shows you have not thought past today, and if you name it, name it as the last resort with the reason — it starts an arms race the next person has to escalate.
"Why does !important reverse for user styles?" Accessibility. A user who forces a minimum font size must be able to beat the author, or the override mechanism would be worthless. It is a rare case of the specification encoding a value judgement, and saying so is a strong signal.
"When would you use :where()?" Writing styles other people should be able to override without escalating — a design system's defaults. It scores zero, so a single plain class beats it.
"Difference between initial, unset and revert?" initial is the specification's default, which is often not what browsers show (display: initial is inline). unset inherits if the property is inheritable and is initial otherwise. revert returns to the browser's own stylesheet, which is usually the one you actually meant.
One thing to volunteer: mention the line-height trap — a unit inherits as a fixed length and overlaps large headings, a unitless number inherits as a multiplier. It is a one-line fix, it shows up in real codebases constantly, and it demonstrates that you understand inheritance passes computed values rather than declarations.
Recall
- The cascade runs four tie-breakers in order: origin + importance → cascade layer → specificity → document order. Each is consulted only if the previous tied, so order is last, not first.
!importantreverses the origin order, and a user!importantbeats an author one — deliberate accessibility design. Transitions outrank even that.@layeris checked before specificity, so a low-specificity rule in a later layer beats a high-specificity rule in an earlier one. Unlayered CSS beats all layered CSS, and!importantinverts layer order too.- Specificity is (ids, classes/attributes/pseudo-classes, types/pseudo-elements) compared left to right. Inline style beats all selectors;
!importantis not part of the score. :is()and:not()take their most specific argument's score;:where()always scores zero, which is what a design system should use.- Inheritance handles the absence of a declaration. Text properties inherit, box properties do not, and the split is practical rather than arbitrary.
inherit/initial/unset/revert/revert-layer— andinitialis not the browser default (display: initialisinline);revertis usually what you meant.line-heightwith a unit inherits a fixed length and overlaps big text; unitless inherits a multiplier. Percentage font sizes compound down the tree.- Computed value is what children inherit; used value needs layout, which is why a
display: noneelement reportswidth: auto.
Self-test: Why does #checkout .btn beat .btn.btn-danger even when written first? · Why can a user's !important beat yours? · What does :where() score, and why is that useful? · Which of initial and revert gives a <div> back its display: block? · Why does a unit-bearing line-height on <body> break headings?
Next: 6.2.3 takes the values the cascade produced and asks what they mean physically — how a box's size is computed from six competing measurements, which unit to reach for and why, and how custom properties behave differently from every other value in CSS.