ESC

Type to search the knowledge base.

CSS Specificity Deep Dive

Count IDs, classes, and elements correctly — :is/:where/:not, inline styles, !important, and how layers change who actually wins.

beginner3 min read
  • css
  • specificity
  • cascade

Specificity is the cascade’s tie-breaker when origin, importance, and layer are equal. Miscounting it is why “I added a class and nothing happened” is a daily stand-up topic. Master the scoreboard, then design systems that rarely need to look at it.

Docs: MDN Specificity, Cascade.

The scoreboard

Think of a tuple (IDs, classes/attributes/pseudo-classes, elements/pseudo-elements). Inline styles sit above that tuple (except vs !important games).

Selector IDs Classes Elements
* 0 0 0
div 0 0 1
.card 0 1 0
.card.featured 0 2 0
#nav 1 0 0
a:hover 0 1 1
input[type="text"] 0 1 1
div.card > p::first-line 0 1 2
.nav a {
  color: blue;
} /* 0,1,1 */
a.nav-link {
  color: red;
} /* 0,1,1 — later wins if equal */
#nav a {
  color: green;
} /* 1,0,1 — beats both */

There is no carry: 11 classes do not equal 1 ID.

Pseudo-classes that rewrite the count

:where() — zero specificity

:where(.token, .chip, .tag) {
  border-radius: 999px;
}
.chip {
  background: var(--brand); /* easily overrides */
}

Ideal for base component defaults you want to override with one class.

:is() / :not() — specific argument wins

:is(.card, #hero) .title {
  /* specificity includes #hero when that branch matches */
  font-weight: 700;
}

A “harmless” :is() that includes an ID can make the whole selector ID-level. :not(.a, .b) uses the most specific argument inside.

:has()

Counts like a pseudo-class, plus the specificity of its relative selector argument (similar spirit to :is()).

.card:has(img) {
  /* class + contents of :has() */
  padding: 0;
}

Inline and !important

<p class="note" style="color: red">…</p>
.note {
  color: blue !important; /* important author vs inline — importance layer rules apply */
}

!important escalates importance, which is resolved before normal specificity comparisons within the appropriate origin/layer. Teams that spray !important lose the ability to reason locally.

Layers beat specificity across buckets

@layer components, utilities;

@layer components {
  #widget .button {
    background: black;
  }
}

@layer utilities {
  .bg-brand {
    background: var(--brand);
  }
}

Utility wins despite lower specificity because later layer wins for normal declarations. Learn layers with specificity — not as a replacement for counting, but as a way to stop counting for overrides.

Debugging method

  1. DevTools → computed → find property → which rule won?
  2. Struck-through rules: note specificity badges / layer labels.
  3. Check inline styles and !important.
  4. Check shadow DOM / user-agent / extension styles if truly impossible.

Interview out-loud

“Specificity compares IDs, then classes/attributes/pseudo-classes, then elements/pseudo-elements. Inline styles beat those; !important changes the importance tier. :where() is zero, :is() takes the highest argument. Equal specificity falls to source order. Cascade layers can make a weaker selector win across layers. I prefer flat classes and layers over ID wars.”

Footguns

  1. IDs in app CSS.
  2. Nested Sass producing accidental super-specific selectors.
  3. :is(#a, .b) poisoning specificity.
  4. Fighting a layer with specificity instead of fixing layer order.
  5. Element selectors in deep chains (nav ul li a span) for cosmetics.

Further reading

Related guides