Which color wins?
#main p { color: blue; }
.intro { color: red; }
<p id="main" class="intro">Hello</p>
↓
which color?Answer: blue. Why? Because #main p is more specific. What does "more specific" actually mean? This guide walks through CSS selector matching, the (a, b, c) specificity calculation, the cascade order, :where()'s zero specificity, !important as the last resort, and how @layer brings sanity to large codebases.
The cascade — how CSS picks a winner
When multiple rules match an element, this order decides:
- Origin & importance — User agent / User / Author.
!importantflipped at each level - Layer order — explicit
@layerordering (modern addition) - Specificity — the (a, b, c) tuple
- Source order — last declared wins among ties
Specificity — the (a, b, c) tuple
Per selector:
a = number of ID selectors (#main = 1)
b = classes / attributes / pseudo-classes (.intro, [type], :hover = 1 each)
c = types / pseudo-elements (p, ::before = 1 each)
Comparison:
(1, 0, 0) > (0, 99, 99) ← one ID beats 99 classes
(0, 1, 0) > (0, 0, 99) ← one class beats 99 typesSpecificity of common selectors:
| Selector | (a, b, c) |
|---|---|
* | (0, 0, 0) |
p | (0, 0, 1) |
p::before | (0, 0, 2) |
.intro | (0, 1, 0) |
p.intro | (0, 1, 1) |
[type="text"] | (0, 1, 0) |
:hover | (0, 1, 0) |
#main | (1, 0, 0) |
#main p | (1, 0, 1) |
#main #header | (2, 0, 0) |
Inline style="..." sits at a separate ~1000 tier — beats almost every selector. !important trumps everything.
:where() — zero-specificity magic
Standardized in 2021. A gift for framework authors:
/* Regular :is() — adopts the highest inner specificity */
:is(.btn, #header) p ← (1, 0, 1) ← #header dominates
/* :where() — always (0, 0, 0) */
:where(.btn, #header) p ← (0, 0, 1) ← ignores inner specificityWhere it shines:
- CSS resets / frameworks — users override with plain selectors
- Tailwind utility wrappers — sidestep specificity wars
- Large component libraries — keep base styles easy to override
/* CSS reset */
:where(h1, h2, h3, h4, h5, h6) {
margin: 0;
font-weight: 600;
}
/* User override — plain selectors work */
h1 { margin: 1rem; } /* (0, 0, 1) > (0, 0, 0) → wins */!important — the last resort
p { color: blue !important; }
#main p { color: red; }
→ blue (important wins)
# When important meets important?
p { color: blue !important; } (0, 0, 1)
#main p { color: red !important; } (1, 0, 1) ← winsImportant cascade:
- User agent (browser default) important
- User stylesheet important
- Author CSS important
- Animations
- Author CSS (normal)
- User stylesheet (normal)
- User agent (normal)
For accessibility, user-stylesheet important beats author important — a vision-impaired user can force a font size that even !important author CSS can't undo.
@layer — modern cascade control (2022)
Specificity + source order isn't enough for big projects. @layer adds explicit ordering:
@layer reset, framework, components, utilities;
@layer reset {
* { margin: 0; }
}
@layer framework {
.btn { background: blue; } /* (0, 1, 0) */
}
@layer components {
.btn-primary { background: green; } /* (0, 1, 0) */
}
@layer utilities {
.bg-red { background: red !important; }
}
/* Result — layer order trumps specificity:
utilities > components > framework > reset
Specificity only matters within a single layer. */Wins:
- Frameworks (Bootstrap, Tailwind) sit above reset cleanly
- Component CSS overrides framework without
!important - Inside a single layer, specificity still applies normally
Tailwind v4 ships with three layers by default (base / components / utilities). Bootstrap 5.3+ adopted layers too.
The cascade's partner — inheritance
Separate from specificity. Some properties inherit from the parent:
Inheritable:
color, font, line-height, letter-spacing, visibility, cursor, ...
Non-inheritable:
background, border, margin, padding, position, display, ...When no rule matches, the parent's inherited value applies. A matching rule beats the inherited value.
Forcing inheritance:
* {
color: inherit; /* every element inherits color from its parent */
}
button {
background: inherit; /* override the browser default bg */
}Debugging specificity in practice
1. Browser DevTools
The Computed tab in the element inspector shows which rule wins. Strike-through marks the losers.
2. Specificity calculator
Compute (a, b, c) for any selector. Keegan Street's specificity.keegan.st is the classic.
3. "Why isn't this rule applying?" checklist
- Typo? Class name correct?
- Specificity lower than the actual winner? DevTools strike-through tells you
- Cascade order — same specificity → last declared wins. Are you earlier in the file?
- Inline style?
style="..."takes priority - Who added
!important? - Inside an
@layer? Check the layer order
Common pitfalls
1. ID overuse
/* Bad — ID is too specific */
#sidebar { width: 200px; }
.sidebar-collapsed { width: 50px; } /* doesn't apply — (0,1,0) < (1,0,0) */
/* Good — class only */
.sidebar { width: 200px; }
.sidebar.collapsed { width: 50px; } /* (0,2,0) > (0,1,0) → wins */2. Deep nesting
/* Bad */
.header .nav .menu .item a { color: blue; } /* (0, 4, 1) */
/* Anyone overriding needs (0, 5, 0)+ → painful */
/* Good */
.menu-item { color: blue; } /* (0, 1, 0) */3. !important inflation
Once you add !important to "fix" one rule, the next override needs another. Specificity loses meaning. Audit the cascade before reaching for it.
4. Tailwind + custom CSS collisions
<div class="bg-blue-500" style="background: red">
↓
inline style wins (Tailwind class < inline)
<div class="bg-blue-500">
<style>
.bg-blue-500 { background: red !important; }
</style>
↓
same specificity + important → source order decidesTailwind v4's @layer utilities structure cleans this up.
5. CSS-in-JS hash classes
styled-components / emotion generate .css-abc123 classes with specificity (0, 1, 0). User overrides need at least (0, 1, 0) to win. Wrapping styles in :where() drops the inner specificity to (0, 0, 0), making overrides trivial.
How selectors actually match
For performance, browsers match selectors right-to-left:
Selector: .container .item .button
Start from every .button in the DOM
→ any ancestor with .item?
→ any ancestor of that ancestor with .container?
→ match
→ otherwise skipDeep nesting is inefficient — every candidate element walks the ancestor chain. Modern browsers index aggressively but flat selectors still win.
References
- MDN — CSS Specificity — MDN
- CSS Cascade Module Level 5 — W3C
- CSS @layer (Bramus Van Damme) — bram.us
- Specificity calculator — specificity.keegan.st
Summary
- Decision order — origin/importance → layer → specificity → source order.
- Specificity is the tuple (a, b, c). a = IDs, b = classes / attributes / pseudo-classes, c = types / pseudo-elements.
- Inline style sits at a separate ~1000 tier.
!importantbeats everything. :where()= zero specificity. Useful for resets and frameworks.@layer(2022) is explicit cascade control. Layer order beats specificity. Standard for large CSS codebases.- Inheritance is separate — some properties pass through; others don't.
- Debug with DevTools' Computed tab + specificity calculators.
- Try it — CSS Formatter to tidy CSS and eyeball each selector's (a, b, c).