CSSMedium

How does CSS selector specificity work?

01

The Short Answer

CSS specificity is the algorithm browsers use to decide which CSS rule wins when multiple rules target the same element. It's a scoring system — the more specific a selector is, the higher its score, and the higher-scoring rule takes precedence regardless of source order.

02

The Specificity Hierarchy

Specificity is calculated as a tuple of four categories, often written as (a, b, c, d). Each category outranks all lower categories combined — a single ID selector beats any number of class selectors.

  • Inline styles (a) — highest specificity
    • Written directly on the element via the `style` attribute
    • Score: (1, 0, 0, 0)
  • IDs (b) — very high specificity
    • Each `#id` in the selector adds 1 to this column
    • Score contribution: (0, 1, 0, 0) per ID
  • Classes, attributes, pseudo-classes (c)
    • `.class`, `[type="text"]`, `:hover`, `:focus`, `:nth-child()`
    • Score contribution: (0, 0, 1, 0) per item
  • Elements and pseudo-elements (d) — lowest specificity
    • `div`, `p`, `span`, `::before`, `::after`
    • Score contribution: (0, 0, 0, 1) per item
03

Calculating Specificity

To calculate specificity, count how many of each category appear in the selector. The following examples show how different selectors score. Notice how a single ID outranks multiple classes, and how combining selectors adds up their individual contributions.

specificity-scores.csscss
/* (0, 0, 0, 1) — one element */
p { color: black; }

/* (0, 0, 1, 0) — one class */
.text { color: blue; }

/* (0, 0, 1, 1) — one class + one element */
p.text { color: green; }

/* (0, 0, 2, 1) — two classes + one element */
div.card.active { color: purple; }

/* (0, 1, 0, 0) — one ID (beats ALL class combinations) */
#header { color: red; }

/* (0, 1, 1, 1) — one ID + one class + one element */
#header .nav a { color: orange; }

/* (0, 2, 0, 0) — two IDs */
#page #header { color: crimson; }

The key insight is that categories never overflow into each other. Even 100 class selectors cannot beat a single ID selector. Each column is compared independently from left to right.

04

A Practical Example

Here's a common scenario that trips people up. Three rules target the same button, but they have different specificities. Even though the last rule appears later in the stylesheet (which normally wins in a tie), specificity takes precedence over source order.

specificity-conflict.csscss
/* Rule 1: (0, 1, 0, 0) — wins because ID beats everything below */
#submit-btn {
  background: red;
}

/* Rule 2: (0, 0, 2, 0) — two classes, still loses to one ID */
.form .btn-primary {
  background: blue;
}

/* Rule 3: (0, 0, 1, 1) — one class + one element, lowest */
button.btn-primary {
  background: green;
}

/* Result: the button is red — the ID selector wins */
/* Even though Rule 3 appears last, specificity > source order */
05

Special Cases

!important

!important overrides all specificity calculations. It creates a separate layer that always wins over normal declarations. If two rules both use !important, then specificity is compared between them. It's a last resort — using it regularly creates maintenance nightmares.

The :where() pseudo-class

:where() is unique because it always contributes zero specificity, regardless of what's inside it. This is useful for writing default styles that are easy to override. Compare it with :is(), which takes the specificity of its most specific argument.

where-vs-is.csscss
/* :where() — specificity is (0, 0, 0, 0) regardless of contents */
:where(#header .nav) a {
  color: blue; /* Easy to override with just: a { color: red; } */
}

/* :is() — takes specificity of most specific argument */
:is(#header, .nav) a {
  color: blue; /* Specificity: (0, 1, 0, 1) because #header is the most specific */
}

Universal selector and combinators

The universal selector (*), combinators (>, +, ~, ), and the negation pseudo-class container (:not() itself) contribute zero specificity. However, the arguments inside :not() do count.

06

Common Mistakes

🔨

Using !important to fix specificity wars

When a style doesn't apply, developers often reach for `!important` instead of understanding why the specificity is wrong. This creates a cascade of `!important` declarations that become impossible to maintain.

Reduce the specificity of the winning rule or increase the specificity of your rule by adding a class. Never use `!important` as a first solution.

🆔

Over-relying on ID selectors

IDs have such high specificity that they make styles very hard to override without resorting to more IDs or `!important`. This creates a specificity arms race.

Prefer class selectors for styling. Reserve IDs for JavaScript hooks and anchor links. A single class is almost always sufficient.

📏

Thinking specificity is a single number

Some tutorials teach specificity as a single score (e.g., IDs = 100, classes = 10, elements = 1). This is misleading — 11 classes do NOT beat 1 ID. The columns never overflow.

Think of specificity as a tuple (a, b, c, d) where each column is compared independently from left to right.

07

Why Interviewers Ask This

Specificity bugs are one of the most common CSS issues in production codebases. Interviewers ask this to check whether you can debug styling conflicts without blindly adding !important, whether you understand how the cascade resolves competing rules, and whether you write maintainable selectors that don't create specificity wars. It's a fundamental CSS concept that separates developers who truly understand the language from those who just trial-and-error their way through styling.

Quick Revision Cheat Sheet

Inline styles: Highest specificity — (1, 0, 0, 0)

IDs: One ID beats any number of classes — (0, 1, 0, 0)

Classes/attrs/pseudo-classes: Middle tier — (0, 0, 1, 0) each

Elements/pseudo-elements: Lowest — (0, 0, 0, 1) each

!important: Overrides all specificity — use as last resort only

:where(): Always zero specificity — great for overridable defaults