CSSCalculating Specificity

Calculating Specificity

Once you understand what the three specificity buckets mean — ID, class/attribute/pseudo-class, type/pseudo-element — calculating specificity is mechanical. You count the components, fill in the columns, and compare. This page gives you practice with real selectors so that specificity becomes instinctive rather than something you have to consciously work through.

The counting rules
  • Count the number of ID selectors (#id) → column A

  • Count the number of class selectors (.class), attribute selectors ([attr]), and pseudo-classes (:hover, :nth-child()) → column B

  • Count the number of type selectors (element) and pseudo-elements (::before) → column C

  • Ignore: the universal selector *, combinators ( , >, +, ~), and :where()

  • For :is(), :not(), :has() — use the specificity of the most specific argument inside them

Worked examples

CSS
/* Count each component type: */

a
/* 1 type → (0, 0, 1) */

.card
/* 1 class → (0, 1, 0) */

#sidebar
/* 1 ID → (1, 0, 0) */

div.card
/* 1 type + 1 class → (0, 1, 1) */

ul > li:first-child
/* 2 types (ul, li) + 1 pseudo-class (:first-child) → (0, 1, 2) */

a:hover::after
/* 1 type (a) + 1 pseudo-class (:hover) + 1 pseudo-element (::after) → (0, 1, 2) */

#nav > ul > li > a
/* 1 ID (nav) + 3 types (ul, li, a) → (1, 0, 3) */

.card.featured .btn:hover
/* 3 classes (.card, .featured, .btn) + 1 pseudo-class (:hover) → (0, 4, 0) */

input[type="text"]:focus
/* 1 type (input) + 1 attribute ([type]) + 1 pseudo-class (:focus) → (0, 2, 1) */
The tricky ones: :is(), :not(), :has()

CSS
/* :is() takes the specificity of its most specific argument */

:is(h1, h2, h3)
/* Most specific arg: h1 → type → (0, 0, 1) */

:is(.card, #sidebar)
/* Most specific arg: #sidebar → ID → (1, 0, 0) */
/* Even when it matches .card, the specificity is (1,0,0) — this is the surprise */

:is(.card, .featured) a
/* Most specific arg: .card (both are (0,1,0)) → (0, 1, 1) */

/* :not() same rules — takes its argument's specificity */
p:not(.hidden)
/* p: (0,0,1) + :not(.hidden): takes (.hidden) → (0,1,0) = total (0,1,1) */

p:not(#special)
/* p: (0,0,1) + :not(#special): takes (#special) → (1,0,0) = total (1,0,1) */

/* :where() always contributes (0,0,0) regardless of arguments */
:where(h1, #main, .featured)
/* (0, 0, 0) — the entire :where() contributes nothing */

/* :has() — same as :is() */
.card:has(img)
/* .card: (0,1,0) + :has(img): takes (img) → (0,0,1) = total (0,1,1) */

.card:has(#featured-img)
/* .card: (0,1,0) + :has(#featured-img): takes (#featured-img) → (1,0,0) = total (1,1,0) */
Mixing IDs and simple selectors inside :is() makes ALL matches take the ID's specificity — even when the ID isn't the one matching
If you write `:is(#header, p) { color: red }` to style both the header and all paragraphs, the rule matches `<p>` elements with specificity `(1,0,0)` — same as an ID selector. This can create unexpected wins over your class-based rules. Either keep `:is()` arguments at the same specificity level, or split them into separate rules.
Comparing two selectors step by step

CSS
/* Which wins? */

/* Selector A */ nav.primary > ul > li.active > a:hover
/* Selector B */ #main-nav a

/* Calculate A:
   nav    → type    → C
   .primary → class  → B
   ul     → type    → C
   li     → type    → C
   .active → class  → B
   a      → type    → C
   :hover → pseudo  → B
   Total: A=0, B=3, C=4 → (0, 3, 4)
*/

/* Calculate B:
   #main-nav → ID   → A
   a         → type → C
   Total: A=1, B=0, C=1 → (1, 0, 1)
*/

/* Compare:
   A column: 0 vs 1 → Selector B wins immediately.
   Despite Selector A having 7 selector components, the single ID in B beats it all.
*/
Quick reference

Selector

Specificity

*

(0, 0, 0)

h1

(0, 0, 1)

.card

(0, 1, 0)

[disabled]

(0, 1, 0)

a:hover

(0, 1, 1)

li::before

(0, 0, 2)

p.intro

(0, 1, 1)

.card.featured

(0, 2, 0)

#nav

(1, 0, 0)

#nav .link

(1, 1, 0)

style="..."

(1, 0, 0, 0) — above all selectors

!important

Outside specificity — separate layer

Practical strategy: don't let specificity grow

The goal isn't to calculate specificity manually for every rule — it's to write selectors that stay at a consistently low level so you rarely need to care. Here's the practical approach most experienced CSS developers follow:

  • Default to single-class selectors (0, 1, 0) for components.

  • Use two-class selectors (0, 2, 0) for component variants and states.

  • Avoid ID selectors in CSS entirely — save #id for JavaScript and anchor links.

  • Avoid deeply nested selectors.nav .list .item .link couples CSS to HTML structure.

  • When you need to override something, add a class — don't increase specificity on the original.

  • When you're fighting specificity, it's almost always a sign to refactor the selector, not reach for !important.

Online tools like Specificity Calculator (specificity.keegan.st) let you paste any selector and see its score — useful when debugging unexpected cascade behaviour
When a style isn't applying and you can't figure out why, DevTools is your first stop. Select the element, look in the Styles panel for the struck-through declaration, and the rule above it is the winner. If the winner is a rule you don't recognise, click the file name to jump to it. DevTools in Chrome also shows the specificity score if you hover over a selector.
Next
How CSS shares values from parent to child without you having to repeat them: [Inheritance](/css/inheritance).