How CSS Works

Chapter 1 — How CSS Works: Cascade, Inheritance, and Specificity

CSS stands for Cascading Style Sheets — and that single word, cascading, describes the entire engine that decides which rule wins when multiple rules compete. Understanding this engine is what separates developers who fight CSS from those who reason about it. This chapter is a deep, authoritative treatment of all three mechanisms: the cascade, inheritance, and specificity.

1. The Cascade — The Full Algorithm

When the browser needs to apply a property to an element, it collects every declaration that targets that element and could set that property. It then runs those declarations through a filter in strict priority order. The first filter that produces a single winner stops — the rest are discarded. Only if all filters tie does the next one apply.

1
Origin and importance
Declarations are ranked by who wrote them and whether they carry !important. The full priority order (lowest to highest):
  1. User-agent stylesheet (browser defaults)
  2. User stylesheet (browser accessibility overrides)
  3. Author stylesheet (your CSS)
  4. Author !important
  5. User !important (accessibility wins over author !important)
  6. User-agent !important
Most of the time only levels 3 and 4 are in play. Level 4 beats level 3 for the same property.
↓ tie? continue
2
CSS layers (@layer)
If @layer is used, declarations in later-declared layers beat those in earlier-declared layers. Un-layered styles beat all layers. This is a relatively new addition to the cascade (covered in depth in the CSS Advanced course).
↓ tie? continue
3
Scoping proximity
With @scope (very new — late 2023+), rules in a more tightly scoped block win. In practice you'll rarely encounter this yet.
↓ tie? continue
4
Specificity
The selector with the highest specificity score wins. This is where most real-world cascade conflicts are resolved. Full details in section 3.
↓ tie? continue
5
Order of appearance
If everything else is equal, the declaration that appears later in the source wins. This is the final tiebreaker — and the reason linking a reset stylesheet before your own CSS is important.
The cascade is a filter, not a priority list. Each stage is only reached if the previous stage couldn't resolve the conflict. Two declarations at different origins never compare specificities — origin always wins first. Two declarations from the same origin and with the same specificity are resolved purely by order, not by any property-level logic.

2. !important — What It Actually Does

!important bumps a declaration to a higher origin tier. It does not make a declaration "infinitely specific" — it moves it to a different layer of the cascade where the filter restarts. Two !important declarations from the same origin still compete on specificity, then order.

/* Author stylesheet */ .card { color: blue; /* author normal — level 3 */ } p { color: red !important; /* author !important — level 4 */ } /* Result on a <p class="card">: RED — !important beats specificity */
!important is a last resort, not a debugging shortcut. Every !important you add makes the next conflict harder to reason about — you'll need another !important with higher specificity to override it. Legitimate uses: third-party stylesheet overrides, user accessibility stylesheets, and utility classes designed to always win (e.g. .hidden { display: none !important; }). If you're using it frequently to fix bugs, the real problem is a specificity architecture issue.

3. Specificity — The Scoring System

Specificity is a three-column score written as (IDs, Classes, Elements). Each selector contributes points to exactly one column based on the type of selector it contains. Columns are compared left to right — a single ID beats any number of classes, regardless of how many classes are stacked.

A
IDs
#id
B
Classes / Attrs / Pseudoclasses
.class · [attr] · :hover · :nth-child()
C
Elements / Pseudoelements
div · p · ::before · ::after

Specificity score reference

SelectorScore (A-B-C)Notes
* 000 Universal selector contributes nothing
p 001 One element type
p::before 002 Pseudo-elements count as element
.card 010 One class
[type="text"] 010 Attribute selectors count as class
:hover 010 Pseudo-classes count as class
:is(h1, h2) 001 :is() takes the specificity of its most specific argument
:where(h1, h2) 000 :where() always contributes zero — intentionally low-specificity
:not(.active) 010 :not() takes the specificity of its argument (.active = class)
.card p 011 One class + one element
nav .card p 012 One class + two elements
#header 100 One ID — beats any number of classes
#header .nav a:hover 121 1 ID + 2 classes/pseudoclasses + 1 element
style="" 100 × ∞ Inline styles have a separate column above IDs — they always beat selectors

Reading specificity scores — worked examples

/* Which rule wins on <p class="intro" id="lead">? */ #lead { color: red; /* (1-0-0) — 1 ID */ } .intro.intro.intro.intro.intro.intro.intro.intro.intro.intro.intro { color: blue; /* (0-11-0) — 11 classes, still loses to 1 ID */ } /* Result: RED — column A (IDs) is compared first. 1 > 0, full stop. */ /* Class column only matters if ID column is tied. */
/* Specificity tie broken by source order */ .card .title { font-size: 1.2rem; /* (0-2-0) */ } .article .title { font-size: 1.4rem; /* (0-2-0) — same score */ } /* On <h2 class="title"> inside both .card and .article: */ /* Both selectors match with equal specificity — LAST ONE WINS */ /* Result: 1.4rem (because .article .title appears later) */

Specificity and the :is(), :not(), :has() gotchas

/* :is() takes the highest specificity of its argument list */ :is(#header, .nav) a { color: blue; /* Score: (1-0-1) — #header pushes the whole :is() to ID level */ /* Even if the element only matches .nav, the score is still 1-0-1 */ } /* :where() always gives zero — great for utility/reset CSS */ :where(#header, .nav) a { color: blue; /* Score: (0-0-1) — :where() contributes nothing */ } /* :has() takes the specificity of its argument */ .card:has(img) { padding: 0; /* Score: (0-1-1) — .card (class) + img (element) */ }

4. Inheritance

Some CSS properties automatically pass their computed value down to child elements if no other rule sets them. This is inheritance. It's what lets you set font-family on body and have it apply to every element on the page without repeating yourself.

Inheritance only kicks in when no other value is specified for the element — it is the last resort after the cascade finds nothing. The cascade always beats inheritance.

Inherited by default
color
font-family
font-size
font-weight
font-style
line-height
letter-spacing
text-align
text-transform
visibility
cursor
list-style
Not inherited by default
margin
padding
border
background
width / height
display
position
box-shadow
transform
overflow
flex / grid properties
animation

Controlling inheritance — the four keywords

.child { color: inherit; /* Force inheritance — use parent's computed value */ color: initial; /* Reset to CSS spec default (often browser default) */ color: unset; /* If inheritable: inherit. If not: initial. Smart reset. */ color: revert; /* Roll back to user-agent stylesheet value — more natural */ } /* Practical use: reset ALL properties on an element */ .isolated-widget { all: revert; /* every property reverts to user-agent defaults */ }
Prefer unset over initial for resets. initial resets to the CSS specification default, which may differ from what the browser renders by default. color: initial sets colour to black regardless of the user's system theme. unset is smarter — it inherits for inheritable properties and uses initial for the rest, giving more predictable results in most reset scenarios.

Inheritance vs the cascade — order of resolution

1. Cascade wins — a matching selector set a value explicitly
highest
2. Inheritance — no cascade winner; parent's computed value flows down (inheritable props only)
3. Browser default (initial value) — no cascade, no inheritance, use the spec default
lowest

5. Computed, Used, and Resolved Values

CSS has multiple stages of value processing. Understanding them clarifies why DevTools sometimes shows a different value than what you wrote:

/* You write: font-size: 1.2em (specified value) */ /* Browser resolves em: font-size: 19.2px (computed value — em resolved to px) */ /* After layout: font-size: 19px (used value — after rounding) */ /* DevTools shows: 19.2px (resolved value — usually the computed value) */ body { font-size: 16px; } .card { font-size: 1.2em; } /* computed: 19.2px */ .card p { font-size: 1.2em; } /* computed: 23.04px — compounding! */

This compounding is why em for font-size can produce unexpected results in nested elements — each level multiplies the parent's computed value. Use rem (root em) to reference the root font-size consistently instead:

html { font-size: 16px; } /* root: 16px */ .card { font-size: 1.2rem; } /* 19.2px — always relative to root */ .card p { font-size: 1.2rem; } /* 19.2px — same, no compounding */

6. Practical Strategies

Keep specificity low by default

/* Avoid: high-specificity selectors are hard to override */ nav ul li a.active { /* (0-1-3) */ color: blue; } /* Better: one class does the job with less weight */ .nav-link--active { /* (0-1-0) — easy to override later */ color: blue; }

Use :where() to write zero-specificity base styles

/* A reset that any single class can override */ :where(h1, h2, h3, h4, h5, h6) { margin: 0; font-weight: bold; /* Score: (0-0-0) — any class beats this */ } .hero-title { font-weight: 900; /* (0-1-0) — wins easily */ }

Leverage inheritance instead of repeating values

/* Bad: repeating font declarations everywhere */ h1, h2, h3, p, li, td, label { font-family: 'Inter', sans-serif; } /* Good: set once on body, let inheritance do the work */ body { font-family: 'Inter', sans-serif; color: #1a1a1a; line-height: 1.6; } /* Every element inherits these automatically */

Debug cascade conflicts with DevTools

In Chrome/Firefox DevTools, select an element and open the Styles panel. You'll see every matching declaration, sorted by cascade priority. Overridden declarations are shown with a strikethrough. Hover over the selector to see its specificity score. This is the fastest way to diagnose "why isn't my CSS applying?"

Chapter Summary

ConceptKey point
CascadeA five-stage filter: origin → @layer → scope → specificity → order. First stage that produces a clear winner stops. !important changes origin tier, not specificity.
SpecificityThree-column score (IDs, Classes, Elements). Compared left to right — 1 ID beats infinite classes. Inline styles are above IDs. !important is above inline styles.
:is() / :has()Take specificity of their most specific argument. :where() always contributes zero — use for intentionally low-specificity base styles.
InheritanceTypographic and text properties inherit by default; box model and layout properties do not. Cascade always beats inheritance.
inherit / initial / unset / revertKeywords to explicitly control inheritance. Prefer unset over initial for resets. all: revert resets every property at once.
em vs remem is relative to the parent's computed font-size and compounds. rem is always relative to the root — predictable and non-compounding.
Specificity strategyKeep selectors at one-class specificity where possible. Use :where() for base/reset styles. Avoid IDs in CSS. Reserve !important for genuine utility overrides.
Exercises
  1. Specificity scoring: Without a browser, work out the specificity score for: body main article.feature > p:first-child. Then check your answer in DevTools.
  2. Inheritance experiment: Set color: hotpink on body and border: 2px solid hotpink on body. Which elements inherit the colour? Which inherit the border? Why?
  3. :where() rewrite: Take this selector — header nav ul li a — and rewrite it using :where() so it has zero specificity but still targets the same elements.
  4. The !important trap: Write two rules targeting the same element with !important on both. Observe which wins and explain why using cascade step 1 and step 4.
  5. em compounding: Set html { font-size: 16px }, then nest three elements each with font-size: 1.25em. Calculate the final font-size at the deepest level. Rewrite using rem and confirm the size stays constant.
Next: Chapter 2 — The Fundamentals: Box Model, Display, and Positioning. A deep dive into how CSS sizes and positions elements — the box model in all its detail, every value of display, and all five positioning modes with practical use cases for each.