CSS

What is CSS selector specificity and how does it work?

The short answer

Specificity is the algorithm the browser uses to decide which CSS rule wins when multiple rules target the same element. It's not about which rule comes first or last in your stylesheet — it's about which selector carries more weight. A more specific selector always beats a less specific one, regardless of source order.

Think of it as a scoring system. Each selector gets a score, and the highest score wins. If two selectors tie, then source order breaks the tie (last one wins).

How the scoring works

Specificity is calculated as a tuple of three numbers, often written as (a, b, c):

  • a — the number of ID selectors (#header, #nav)
  • b — the number of class selectors, attribute selectors, and pseudo-classes (.active, [type="text"], :hover)
  • c — the number of element selectors and pseudo-elements (div, p, ::before)

You compare these numbers from left to right. A higher number in an earlier position always wins, no matter how large the later numbers are.

/* (0, 0, 1) — one element selector */
p {
color: black;
}

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

/* (1, 0, 0) — one ID selector */
#hero {
color: red;
}

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

A single ID selector (1, 0, 0) beats any number of class selectors. You could have .a.b.c.d.e.f.g.h.i.j — that's (0, 10, 0) — and it still loses to #hero at (1, 0, 0). The leftmost column always takes priority.

Practical specificity battles

Let's look at some real examples. Say you have this HTML:

<div id="sidebar">
<ul class="nav-list">
<li class="nav-item active">
<a href="/home">Home</a>
</li>
</ul>
</div>

And these competing rules:

/* (0, 1, 1) — class + element */
.nav-list a {
color: gray;
}

/* (0, 2, 1) — two classes + element */
.nav-list .active a {
color: blue;
}

/* (1, 0, 1) — ID + element */
#sidebar a {
color: green;
}

/* (1, 1, 1) — ID + class + element */
#sidebar .active a {
color: red;
}

The link ends up red because (1, 1, 1) is the highest specificity. Even though #sidebar a has an ID, the last rule adds a class on top of that, pushing it ahead.

Where inline styles and !important fit in

Inline styles sit above the specificity system entirely. A style attribute on an element beats any selector in your stylesheet:

<p id="hero" class="intro" style="color: purple;">
This is purple, no matter what your stylesheet says.
</p>

And then there's !important, which overrides everything — including inline styles:

p {
color: orange !important;
}

The full priority order, from lowest to highest:

  1. Normal stylesheet rules (compared by specificity, then source order)
  2. Inline styles (style="...")
  3. !important declarations (compared by specificity among themselves)
  4. !important inline styles

If two !important rules conflict, specificity decides between them, just like normal rules. It's specificity all the way down.

Why !important is a trap

You might wonder: if !important always wins, why not just use it when you need a style to stick? The problem is escalation. Once you use !important to override something, the only way to override that is another !important with higher specificity. Before long, your stylesheet is full of !important declarations fighting each other, and nobody can tell which rule actually applies without tracing through the entire cascade.

In practice, !important has a few legitimate uses — overriding third-party library styles you can't change, or utility classes in a design system. But if you're using it to fix your own code, it's usually a sign that your selectors are too tangled.

Avoiding specificity wars

The best teams treat specificity as something to keep low, not something to win. Here's how:

Favor classes over IDs in stylesheets. IDs are great for JavaScript hooks and anchor links, but in CSS they create specificity spikes that are hard to override later.

/* Instead of this */
#header .nav .link {
color: blue;
}

/* Do this */
.header-link {
color: blue;
}

Use flat, descriptive class names. Methodologies like BEM (.card__title--highlighted) keep specificity consistently low because every selector is a single class.

Don't nest too deeply. Each level of nesting adds to your specificity score and makes overrides harder:

/* (0, 4, 0) — hard to override without escalation */
.page .sidebar .nav .link {
color: gray;
}

/* (0, 1, 0) — easy to override */
.sidebar-link {
color: gray;
}

Use cascade layers (@layer) in modern CSS. Layers let you control which group of styles takes priority without touching specificity at all. This is particularly useful when combining your styles with third-party libraries.

The universal selector and combinators

A few things have zero specificity:

  • The universal selector (*)
  • Combinators (>, +, ~, )
  • The :where() pseudo-class

This means *.intro has the same specificity as .intro — the * adds nothing. And :where(.active) has (0, 0, 0) specificity, which is useful for writing default styles that are easy to override.

On the other hand, :is() and :not() take on the specificity of their most specific argument:

/* :is(.active, #main) has (1, 0, 0) specificity — the ID wins */
:is(.active, #main) {
color: red;
}

Why interviewers ask this

Specificity questions reveal whether you actually understand the cascade or just trial-and-error your way through CSS. In large codebases, specificity problems cause real pain — styles that mysteriously don't apply, !important scattered everywhere, developers afraid to change anything. Understanding specificity means you can write CSS that's predictable and maintainable, which matters far more than knowing every property by heart.