Specificity in Depth
Level 1 introduced specificity as three columns (ids, classes, types). Modern selectors add some rules worth knowing precisely.
:is(), :where(), and :not()
:is(h1, h2, h3) a { color: inherit; }
:where(.prose) p { margin-block: 1em; }
.button:not(.primary, .danger) { background: #eee; }
:is(a, b)matches if any selector in the list matches. Its specificity is that of its most specific argument.:where(a, b)matches the same way but always has zero specificity — perfect for defaults that should be easy to override.:not(a, b)takes the specificity of its most specific argument, like:is().:has()(later in this level) works like:is()too.
<style>
:is(#main, .card) p { color: red; }
.card p { color: green; }
:where(#main, .card) span { color: red; }
span { color: blue; }
</style>
<div class="card"><p>Paragraph</p><span>Span</span></div>
<script>
const card = document.querySelector(".card");
console.log("p:", getComputedStyle(card.querySelector("p")).color);
console.log("span:", getComputedStyle(card.querySelector("span")).color);
</script>
p: rgb(255, 0, 0)
span: rgb(0, 0, 255)
- The paragraph is red:
:is(#main, .card) pcounts as 1,0,1 because#mainis in the list — even though the element matched through.card. That beats.card p(0,1,1). - The span is blue:
:where(...)contributed nothing, so the rule was 0,0,1, equal tospan, andspancame later.
Keep specificity low and flat
Specificity problems grow: one over-specific selector forces the next
override to be more specific, and so on until !important everywhere.
Habits that prevent this:
- Style with single classes (
.card-title, not.sidebar .card h3). - No ids in selectors.
- Wrap library defaults and resets in
:where()so they're trivially overridden. - Use cascade layers (Level 3) to control priority by group instead of by selector weight.
The cascade order, fully
Specificity only matters between declarations of the same origin,
importance, and layer. Before comparing specificity, the browser
considers — in this order — the origin and !important, the context
(shadow DOM), style attributes, and cascade layers. After specificity
comes scope proximity (@scope), then source order. Level 3's
The Cascade in Depth covers every step.
Practice
Rank these selectors by specificity, highest first:
.nav a:hover, #logo, nav :where(ul, ol) li a, :is(.nav, header) a.
Show solution
#logo— 1,0,0.nav a:hover— 0,2,1:is(.nav, header) a— 0,1,1 (the most specific argument,.nav, counts)nav :where(ul, ol) li a— 0,0,3 (:whereadds nothing)