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 ACount the number of class selectors (
.class), attribute selectors ([attr]), and pseudo-classes (:hover,:nth-child()) → column BCount the number of type selectors (
element) and pseudo-elements (::before) → column CIgnore: the universal selector
*, combinators (,>,+,~), and:where()For
:is(),:not(),:has()— use the specificity of the most specific argument inside them
Worked examples
/* 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()
/* :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) */
Comparing two selectors step by step
/* 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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
#idfor JavaScript and anchor links.Avoid deeply nested selectors —
.nav .list .item .linkcouples 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.