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:

1
Origin and Importance — where the rule came from, and whether it carries !important
2
Specificity — the "weight" of the selector that matched
3
Source Order — when specificity ties, whichever declaration comes last wins

Only 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.

Inheritance is not the cascade. These two mechanisms are often confused. The cascade resolves conflicts between competing declarations. Inheritance is a separate mechanism: when no declaration sets a property on an element, the browser checks if that property inherits from the parent — and if so, uses the parent's computed value. Section 7 covers inheritance in detail.

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.

Why does !important reverse the order? The reversal is deliberate. If author !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 columnB — Class columnC — 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

SelectorABCNotes
*000Universal selector — zero specificity
div001One element type
div p002Two element types (combinator adds 0)
.card010One class
.card p011One class + one element
.card.active020Two classes (chained — no space)
a[href]011Attribute selector counts as class-level
a:hover011Pseudo-class counts as class-level
p::first-line002Pseudo-element counts as element-level
#nav100ID — column A, always beats B and C
#nav .link:hover120ID + class + pseudo-class
:not(.card)010:not() contributes its argument's specificity
:is(h1, .title)010:is() takes highest — .title wins
:where(h1, .title)000:where() always 0,0,0
Live demo — all rules compete for the same element
div#hero.box.active
selector ID CLS EL result
/* Target element: <div id="hero" class="box active"> inside <div class="container"> */

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.

Live demo — same specificity (0·1·0), source order decides
1st in file
2nd in file (LAST)
🎨 CSS — source order in practice
/* 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.

🎨 CSS — !important syntax and behaviour
/* 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

SituationVerdict
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
⚠️ The !important escalation trap. Using !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.

Live demo — toggle properties on parent, watch which reach the child
Parent div — CSS applied here:
Parent text — these styles are set directly on me.
Child div — no CSS set directly:
Child text — I receive inherited properties automatically.

Common inherited properties

CategoryInherited properties
Typographycolor, font, font-family, font-size, font-weight, font-style, font-variant, line-height, letter-spacing, word-spacing, text-align, text-transform, text-indent
Listslist-style, list-style-type, list-style-position
Tablesborder-collapse, border-spacing, caption-side
Visibilityvisibility, cursor

Common non-inherited properties

CategoryNon-inherited properties
Box modelwidth, height, margin, padding, border
Backgroundbackground, background-color, background-image
Layoutdisplay, position, top/right/bottom/left, float
Flex/Gridflex, 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:

🎨 CSS — inherit, initial, unset, revert
.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.

🎨 CSS — @layer basics (preview)
/* 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:

🎨 CSS — specificity strategies
/* ✗ 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

🎨 CSS — recommended file/section order
/* 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 */
💡 DevTools is your best teacher. In Chrome or Firefox DevTools, clicking an element and viewing the "Styles" panel shows every competing declaration for the selected element, each crossed out (overridden) or active. The source of each rule — file name and line number — is shown on the right. This is the fastest way to understand why a style is or is not applying.

9 — Quick Reference

Cascade resolution order (highest wins)

PriorityCategory
1 (highest)Transitions (in progress)
2!important user-agent declarations
3!important user declarations
4!important author declarations (your CSS)
5Animations (in progress)
6Normal author declarations → resolved by specificity → then source order
7Normal 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

KeywordEffect
inheritForce: use parent's computed value (even for non-inheritable properties)
initialReset to the CSS spec's initial value
unsetinherit if the property inherits, else initial
revertReset to the browser's default stylesheet value
all: unsetApply 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.

Exercise 1
Given the HTML <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; }
Hint: Compare column A first. Any rule with an ID selector immediately beats all rules with no ID, regardless of what is in columns B and C.
Specificity scores and winner
/* 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. */
Exercise 2
A junior developer added 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.
Hint: Remember where !important sits in the cascade. The second fix should involve refactoring the original rule to remove !important.
Explanation
/* 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 */
Exercise 3
You have a .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.
Hint: Add context to the selector: .content .widget p has higher specificity than .widget p alone. The same principle applies for .sidebar .widget p.
CSS
.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. */
Exercise 4
A .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.
Hint: Browser user-agent stylesheets set font-size on form elements to a small size. font-size is an inherited property, but user-agent declarations trump inheritance from body.
Answers
/* (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.). */