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):

  1. Origin & importance — user-agent → user → author; !important flips the order
  2. Inline stylesstyle="…" attribute (specificity 1,0,0,0)
  3. Layer order@layer rules (newer CSS feature; last layer wins unless importance flips it)
  4. Specificity — selector weight
  5. 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)
SelectorSpecificityWhy
p0,0,0,11 element
.card0,0,1,01 class
#hero0,1,0,01 ID
style="…"1,0,0,0Inline
nav ul.menu li a:hover0,0,2,42 classes (.menu, :hover) + 4 elements (nav, ul, li, a)
#sidebar .widget h30,1,1,11 ID + 1 class + 1 element

Key rules

  • IDs beat any number of classes0,1,0,0 > 0,0,99,0
  • Classes beat any number of elements0,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 !important loses to user !important (accessibility user stylesheets)
  • Using !important broadly 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

GuidelineWhy
Use classes as the primary styling hookLow, predictable specificity
Avoid styling by IDHigh specificity; hard to override
Keep selectors short.card-title beats main section .card .card-header h3 in maintainability
Use :where() for resets/defaultsZero specificity — easy to override
Reserve !important for true overridesThird-party containment, a11y utilities
Use @layer in larger projectsExplicit, predictable cascade order

6. Key takeaways

  1. The cascade resolves conflicts via origin → specificity → source order.
  2. Specificity is a tuple: (inline, IDs, classes, elements) — categories never overflow into each other.
  3. !important is a scalpel, not a default tool — it creates escalation.
  4. :where() zeroes specificity; :is() takes the highest argument's specificity.
  5. @layer brings explicit ordering to the cascade for large codebases.

Explain-It Challenge

Explain without notes:

  1. Why 10 class selectors still lose to 1 ID selector.
  2. A real scenario where !important is justified (not just laziness).
  3. The difference between :is() and :where() in terms of specificity.

Navigation: ← 1.6.a — Syntax & Selectors · 1.6.c — Inheritance →