Skip to content
AgileLogics
6 min read

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) p counts as 1,0,1 because #main is 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 to span, and span came 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
  1. #logo — 1,0,0
  2. .nav a:hover — 0,2,1
  3. :is(.nav, header) a — 0,1,1 (the most specific argument, .nav, counts)
  4. nav :where(ul, ol) li a — 0,0,3 (:where adds nothing)