Episode 1 — Fundamentals / 1.6 — CSS Core Fundamentals
1.6.b — Specificity & the Cascade
In one sentence: The cascade is the algorithm that decides which CSS declaration wins when multiple rules target the same element — it considers origin, specificity, layer order, and source order, in that priority.
Navigation: ← 1.6.a — Syntax & Selectors · 1.6.c — Inheritance →
1. The cascade — conflict resolution
When two or more declarations set the same property on the same element, the browser resolves the conflict using this priority (highest wins):
- Origin & importance — user-agent → user → author;
!importantflips the order - Inline styles —
style="…"attribute (specificity1,0,0,0) - Layer order —
@layerrules (newer CSS feature; last layer wins unless importance flips it) - Specificity — selector weight
- Source order — last rule in the stylesheet wins (tie-breaker)
For day-to-day work, specificity and source order are the most common tie-breakers.
2. Specificity scoring
Specificity is a weight tuple, not a single number:
(inline, IDs, classes/attrs/pseudo-classes, elements/pseudo-elements)
| Selector | Specificity | Why |
|---|---|---|
p | 0,0,0,1 | 1 element |
.card | 0,0,1,0 | 1 class |
#hero | 0,1,0,0 | 1 ID |
style="…" | 1,0,0,0 | Inline |
nav ul.menu li a:hover | 0,0,2,4 | 2 classes (.menu, :hover) + 4 elements (nav, ul, li, a) |
#sidebar .widget h3 | 0,1,1,1 | 1 ID + 1 class + 1 element |
Key rules
- IDs beat any number of classes —
0,1,0,0>0,0,99,0 - Classes beat any number of elements —
0,0,1,0>0,0,0,99 - Combinators and
*do not add specificity :is()and:not()take the specificity of their most specific argument:where()has zero specificity — useful for defaults you want easily overridden:has()takes the specificity of its argument
3. !important — the override hammer
.alert {
color: red !important;
}
!important jumps the declaration to the top of the origin tier — it beats inline styles and higher-specificity selectors. But:
- Author
!importantloses to user!important(accessibility user stylesheets) - Using
!importantbroadly creates specificity escalation wars — every future override also needs!important - Legitimate uses: accessibility overrides, third-party widget containment, utility classes in frameworks
Rule of thumb: if you reach for !important, first ask whether you can lower the specificity of the competing rule instead.
4. Cascade layers (@layer) — awareness
@layer lets authors define explicit priority tiers:
@layer reset, base, components, utilities;
@layer reset {
* { margin: 0; box-sizing: border-box; }
}
@layer utilities {
.sr-only { /* … */ }
}
Later-declared layers win over earlier ones (for normal declarations). This is how frameworks like Tailwind v4+ avoid specificity wars — utilities sit in the highest layer.
5. Practical specificity strategy
| Guideline | Why |
|---|---|
| Use classes as the primary styling hook | Low, predictable specificity |
| Avoid styling by ID | High specificity; hard to override |
| Keep selectors short | .card-title beats main section .card .card-header h3 in maintainability |
Use :where() for resets/defaults | Zero specificity — easy to override |
Reserve !important for true overrides | Third-party containment, a11y utilities |
Use @layer in larger projects | Explicit, predictable cascade order |
6. Key takeaways
- The cascade resolves conflicts via origin → specificity → source order.
- Specificity is a tuple:
(inline, IDs, classes, elements)— categories never overflow into each other. !importantis a scalpel, not a default tool — it creates escalation.:where()zeroes specificity;:is()takes the highest argument's specificity.@layerbrings explicit ordering to the cascade for large codebases.
Explain-It Challenge
Explain without notes:
- Why 10 class selectors still lose to 1 ID selector.
- A real scenario where
!importantis justified (not just laziness). - The difference between
:is()and:where()in terms of specificity.
Navigation: ← 1.6.a — Syntax & Selectors · 1.6.c — Inheritance →