The Cascade
🌊 Chapter 6 — The Cascade and Specificity in Depth
CSS stands for Cascading Style Sheets — the cascade is not an incidental feature, it is the central algorithm that makes CSS work. When two rules both try to set the same property on the same element, the cascade decides which one wins. Understanding this algorithm precisely — not just roughly — is the difference between CSS that behaves predictably and CSS that feels like a fight you keep losing. This chapter covers the full cascade, specificity scoring, inheritance, and practical strategies for writing CSS that is easy to reason about.
1 — What the Cascade Does
Every CSS property on every element has exactly one computed value. When multiple declarations compete to set the same property, the cascade resolves the conflict in a fixed order. The cascade does not care about your file structure or your intent — it cares about three things, in order of priority:
!importantOnly when step 1 cannot decide a winner does the cascade move to step 2. Only when step 2 ties does it move to step 3. Steps are evaluated in order until a winner is found.
2 — Step 1: Origin and Importance
Every CSS declaration comes from one of three origins:
- User-agent stylesheet — the browser's built-in defaults (buttons look button-like, links are blue and underlined, etc.).
- User stylesheet — custom styles the person browsing has applied (accessibility overrides, custom fonts). Rare in practice but part of the spec.
- Author stylesheet — CSS you write as a developer. This is almost always the only origin you work with.
In the normal (non-!important) cascade, author styles override user styles, which override user-agent styles. Adding !important reverses the order — user-agent !important is strongest, then user !important, then author !important.
!important always beat everything, users with visual impairments who set high-contrast colours in their operating system would have their settings overridden by websites. The spec protects user overrides by making user !important stronger than author !important.
In practice — working entirely in author stylesheets — !important just overrides other author declarations for that property. But it sits at a completely different layer in the algorithm, not in the specificity calculation. This is why a low-specificity rule with !important beats a high-specificity rule without it.
3 — Step 2: Specificity — the A·B·C Score
Specificity is a three-part score written as A · B · C. Each part is an independent integer. They are compared left-to-right — A takes absolute priority over B, B takes absolute priority over C. You cannot "add up" C values to beat a B value: 0·0·1000 is still less specific than 0·1·0.
| A — ID column | B — Class column | C — Element column |
|---|---|---|
+1 for each ID selector (#name) |
+1 for each class (.name), attribute ([attr]), or pseudo-class (:hover, :nth-child()) |
+1 for each element type selector (div, p) or pseudo-element (::before) |
The universal selector * adds 0 to every column. Combinators (+, >, ~, space) add nothing. :not(), :is(), :has() contribute the specificity of their most specific argument. :where() always contributes 0. |
||
Scoring examples
| Selector | A | B | C | Notes |
|---|---|---|---|---|
* | 0 | 0 | 0 | Universal selector — zero specificity |
div | 0 | 0 | 1 | One element type |
div p | 0 | 0 | 2 | Two element types (combinator adds 0) |
.card | 0 | 1 | 0 | One class |
.card p | 0 | 1 | 1 | One class + one element |
.card.active | 0 | 2 | 0 | Two classes (chained — no space) |
a[href] | 0 | 1 | 1 | Attribute selector counts as class-level |
a:hover | 0 | 1 | 1 | Pseudo-class counts as class-level |
p::first-line | 0 | 0 | 2 | Pseudo-element counts as element-level |
#nav | 1 | 0 | 0 | ID — column A, always beats B and C |
#nav .link:hover | 1 | 2 | 0 | ID + class + pseudo-class |
:not(.card) | 0 | 1 | 0 | :not() contributes its argument's specificity |
:is(h1, .title) | 0 | 1 | 0 | :is() takes highest — .title wins |
:where(h1, .title) | 0 | 0 | 0 | :where() always 0,0,0 |
4 — Step 3: Source Order
When two declarations have identical specificity and the same origin, the one that appears later in the stylesheet wins. This is the tiebreaker of last resort — and it is why the order of your CSS rules and <link> tags matters.
/* File order matters — later rules win ties */
.btn { background: #4f8ef7; } /* overridden */
.btn { background: #3a9a6a; } /* wins — same spec, later */
/* Link tag order matters too */
<link rel="stylesheet" href="base.css"> <!-- loaded first -->
<link rel="stylesheet" href="components.css"> <!-- overrides base for ties -->
<link rel="stylesheet" href="overrides.css"> <!-- has the last word -->
/* Practical rule: put more specific/later styles at the bottom of your file */
/* Reset → Base → Layout → Components → Utilities → Page-specific */
5 — !important in Depth
!important elevates a declaration out of the normal specificity stack entirely. It sits in a separate "important" tier of the cascade — above all normal declarations regardless of their specificity.
/* Syntax: goes inside the declaration, before the semicolon */
.card {
color: red !important;
}
/* This beats even a highly specific rule with no !important */
#app #main .card.active span { /* 2·2·1 — very high specificity */
color: blue;
}
/* .card { color: red !important } wins — !important is in a different tier */
/* When two !important rules compete, specificity decides between them */
.card { color: red !important; } /* 0·1·0 */
#hero { color: blue !important; } /* 1·0·0 → wins: higher A */
When !important is legitimate
| Situation | Verdict |
|---|---|
Utility classes that must always apply (.visually-hidden, .sr-only, .d-none) | ✓ Appropriate |
| Overriding third-party CSS (external library, widget, embed) you cannot modify | ✓ Appropriate |
| Accessibility overrides that must not be overridden | ✓ Appropriate |
| Fixing specificity you created yourself but can't be bothered to refactor | ✗ Warning — technical debt |
Fighting against another !important you wrote earlier | ✗ Specificity war — refactor instead |
| Making something "more important" as a general habit | ✗ Almost certainly a design smell |
!important to win a specificity fight creates a new problem: the next person who needs to override you must also use !important, then with higher specificity, then you respond with higher specificity again. CSS becomes unmaintainable. The root fix is almost always to reduce the specificity of the original rule, not to add more !important.
6 — Inheritance
Inheritance is a separate mechanism from the cascade. When no declaration sets a property on an element (the cascade finds no winner), the browser checks if that property is defined as inheritable. If so, it uses the computed value of the nearest ancestor that has it.
Not all properties inherit. Properties that control typography and text generally inherit; properties that control box layout and decoration generally do not.
Common inherited properties
| Category | Inherited properties |
|---|---|
| Typography | color, font, font-family, font-size, font-weight, font-style, font-variant, line-height, letter-spacing, word-spacing, text-align, text-transform, text-indent |
| Lists | list-style, list-style-type, list-style-position |
| Tables | border-collapse, border-spacing, caption-side |
| Visibility | visibility, cursor |
Common non-inherited properties
| Category | Non-inherited properties |
|---|---|
| Box model | width, height, margin, padding, border |
| Background | background, background-color, background-image |
| Layout | display, position, top/right/bottom/left, float |
| Flex/Grid | flex, grid-column, align-self, and all container properties |
Explicit inheritance keywords
You can force or reset inheritance with four special keywords that work on any property:
.child {
/* inherit: force inheritance even for non-inherited properties */
background: inherit; /* background doesn't inherit — this forces it */
color: inherit; /* colour inherits anyway, but explicit is clear */
/* initial: reset to the CSS specification's initial value */
/* (not the browser default — the spec-defined initial value) */
color: initial; /* = 'canvastext' (browser-dependent black) */
display: initial; /* = 'inline' (spec initial, not block) */
/* unset: inherits if property is inheritable, else uses initial */
/* behaves like 'inherit' for color, like 'initial' for display */
all: unset; /* reset ALL properties at once — useful for resets */
/* revert: reset to the browser's default stylesheet value */
/* (the value the browser would apply if you wrote no CSS) */
display: revert; /* div → block, span → inline, etc. */
}
/* all: unset is common in component resets */
.widget {
all: unset; /* wipe the slate — set every property to unset state */
}
initial is the spec-defined default, which is not always the browser default. For example, display: initial gives you inline — the spec's initial value — even on a div. If you want the browser's default (block for div), use revert instead.7 — @layer: A New Tier in the Cascade
CSS now supports a @layer rule that adds a new level to the cascade, sitting between origin and specificity. Layers let you define ordered buckets of CSS where the layer order matters more than specificity — a rule in a higher-priority layer wins even if a rule in a lower-priority layer has higher specificity.
/* Declare layer order — first listed = lowest priority */
@layer reset, base, components, utilities;
/* Assign rules to a layer */
@layer base {
a { color: blue; }
}
@layer utilities {
/* utilities has higher priority than base — */
/* this wins even if .text-red has lower specificity than the base rule */
.text-red { color: red; }
}
/* Rules outside any @layer beat all layered rules */
@layer is a major addition to the cascade that solves specificity wars at the architecture level. It is covered in depth in the CSS Advanced course, Chapter 1. Browser support is excellent (all modern browsers since 2022).8 — Practical: Writing Manageable CSS
Keep specificity low and consistent
The hardest CSS to maintain is CSS with high specificity variance — some rules at 0·0·1, others at 2·3·2. When you need to override something, you have to match or beat the highest score already in your stylesheet. Start flat and stay flat:
/* ✗ Avoid nesting that builds specificity */
#sidebar .widget .widget-title a.active { /* 1·3·1 — very hard to override */
color: red;
}
/* ✓ Give the element its own class — target it directly */
.sidebar-active-link { /* 0·1·0 — easy to override or extend */
color: red;
}
/* ✓ Avoid IDs in selectors entirely — save them for anchors/JS */
/* ✗ #main h2 { … } — the ID pollutes every h2 inside #main */
/* ✓ .page-main h2 { … } — class-level, much easier to manage */
/* ✓ Use :where() for base styles — zero specificity */
:where(h1, h2, h3) {
font-family: system-ui; /* easily overrideable — 0·0·0 */
}
The order that prevents conflicts
/* 1. Reset / normalise — zero or near-zero specificity */
@import "reset.css";
/* 2. Design tokens — custom properties (inherited, low spec) */
:root { --color-primary: #4f8ef7; }
/* 3. Base styles — element selectors, low specificity */
body { font-family: Georgia, serif; }
/* 4. Layout — single class selectors */
/* 5. Components — single class selectors */
/* 6. Utilities — single class, !important where needed */
/* 7. Page-specific overrides — last, highest source order */
9 — Quick Reference
Cascade resolution order (highest wins)
| Priority | Category |
|---|---|
| 1 (highest) | Transitions (in progress) |
| 2 | !important user-agent declarations |
| 3 | !important user declarations |
| 4 | !important author declarations (your CSS) |
| 5 | Animations (in progress) |
| 6 | Normal author declarations → resolved by specificity → then source order |
| 7 | Normal user declarations |
| 8 (lowest) | Normal user-agent declarations (browser defaults) |
Specificity scoring
| A (ID) | B (Class/Attr/Pseudo-class) | C (Element/Pseudo-element) |
|---|---|---|
#id |
.class · [attr] · :hover · :nth-child() |
div · p · ::before |
Explicit inheritance keywords
| Keyword | Effect |
|---|---|
inherit | Force: use parent's computed value (even for non-inheritable properties) |
initial | Reset to the CSS spec's initial value |
unset | inherit if the property inherits, else initial |
revert | Reset to the browser's default stylesheet value |
all: unset | Apply unset to every property at once |
✏️ Exercises
Predict the outcome before revealing the answer — the goal is to reason through the cascade, not just check the result.
<p id="intro" class="lead highlight"> and these four CSS rules all setting color, predict which wins and explain why. Then calculate the specificity score (A·B·C) for each rule.Rule A:
p { color: gray; }Rule B:
.lead { color: blue; }Rule C:
.lead.highlight { color: green; }Rule D:
#intro { color: red; }
/* Rule A: p → 0·0·1 — one element */
/* Rule B: .lead → 0·1·0 — one class */
/* Rule C: .lead.highlight → 0·2·0 — two classes */
/* Rule D: #intro → 1·0·0 — one ID */
/* Winner: Rule D (#intro) — column A = 1 beats all others at 0.
No other rule even has an entry in column A, so the
comparison stops there. Color is red.
If Rule D did not exist:
Rule C (0·2·0) beats Rule B (0·1·0) beats Rule A (0·0·1).
Color would be green. */
color: purple !important to .card to fix a specificity problem, but now they need to override it in .card.featured. They try adding color: gold to .card.featured and it does not work. Explain why, and describe two ways to fix it — one using !important and one without./* Why it fails:
.card { color: purple !important; } is in the "important author"
tier of the cascade. .card.featured { color: gold; } is in the
"normal author" tier. The important tier always beats the normal
tier regardless of specificity. So gold never applies. */
/* Fix 1 — also use !important, with higher specificity */
.card.featured { color: gold !important; }
/* Both are now in the same !important tier.
.card.featured (0·2·0) beats .card (0·1·0) — gold wins. */
/* Fix 2 (preferred) — remove !important, fix the root cause */
/* Before (problem): */
.card { color: purple !important; }
/* After: understand WHY .card needed !important.
Usually it was overriding something with too-high specificity.
Reduce the overriding rule's specificity instead, then remove
the !important — back in the normal cascade, .card.featured
naturally overrides .card. */
.card { color: purple; } /* 0·1·0 */
.card.featured { color: gold; } /* 0·2·0 — wins naturally */
.widget component with paragraphs inside it. The widget is placed in two different contexts: inside .sidebar and inside .content. Write CSS that gives .widget p a default font-size: 0.9rem, overrides it to 1rem when inside .content, and overrides it to 0.8rem when inside .sidebar. Achieve this using only the cascade and specificity — no !important..content .widget p has higher specificity than .widget p alone. The same principle applies for .sidebar .widget p..widget p { font-size: 0.9rem; } /* 0·1·1 — base */
.content .widget p { font-size: 1rem; } /* 0·2·1 — wins in .content */
.sidebar .widget p { font-size: 0.8rem; } /* 0·2·1 — wins in .sidebar */
/* The context selector adds one class (0·1·0) to the score,
making it 0·2·1 vs 0·1·1. Both context selectors score equally
but they are mutually exclusive (an element is in .content OR
.sidebar, not both), so there is no conflict between them. */
.btn element inside a form has font-size set to 16px on body. The button shows at 12px instead. No CSS explicitly sets font-size on .btn. Investigate: (a) explain how it could show at 12px if no rule targets it; (b) write a rule using inherit to force the button to use the body's font size; (c) explain what all: revert would do on .btn.font-size on form elements to a small size. font-size is an inherited property, but user-agent declarations trump inheritance from body./* (a) Why it shows at 12px:
The browser's user-agent stylesheet explicitly sets font-size on
button (and input, select) to a small size — often 11px or 12px.
This is an explicit declaration on the element, which beats
inherited values. Inheritance only applies when NO declaration
targets the property; the UA rule targets it, so body's font-size
is never inherited. */
/* (b) Force inheritance */
.btn {
font-size: inherit; /* author stylesheet beats UA — now uses body's 16px */
}
/* (c) all: revert on .btn
This resets every property to what the browser's default
stylesheet would set. For a button, that means:
- font-size → browser's default for buttons (small)
- border → browser's default button border
- background → browser's default button background
It does NOT give you inherited body styles — revert goes
back to the UA stylesheet, not to inherited values.
Use 'all: unset' if you want to strip browser defaults
and start fresh (then re-add font-size: inherit, etc.). */