CSS Specificity

CSS specificity is the set of rules a browser uses to decide which declaration wins when two or more CSS rules target the same element and the same property. Without it, the cascade would be unpredictable — every page has overlapping selectors (a type selector here, a class there, an ID somewhere else), and specificity is the scoring system that resolves the conflict. Understanding specificity is essential for debugging “why isn’t my style applying?” and for writing CSS that stays maintainable as a project grows.

Specificity is not about which rule is written last (that’s source order, a separate tiebreaker) and it is not a single number you add up in base 10 — it’s a 4-part score compared column by column. This lesson walks through exactly how that score is calculated, how it interacts with the cascade and !important, and the mistakes that cause the most confusion in real projects.

Overview / How it works

When the browser’s rendering engine builds the final styles for an element (part of building the render tree, before layout and paint), it first collects every CSS rule whose selector matches that element. If more than one rule sets the same property, the engine has to pick a winner. It does this in three ordered stages:

  • Origin and importance: user-agent (browser default) styles lose to author (your) styles, which lose to !important author styles, which lose to !important user styles. Within your own stylesheet, a normal declaration always loses to an !important one, regardless of specificity.
  • Specificity: within the same importance tier, the selector with the higher specificity score wins.
  • Source order: if specificity is exactly tied, the declaration that appears later in the CSS (or in a later stylesheet/<style> block) wins.

Specificity itself is calculated as four ordered categories, often written as a tuple (a, b, c, d):

  • a — inline styles. A style attribute directly on an element. Counts as 1 if present, 0 otherwise.
  • b — ID selectors. Each #id in the selector adds 1 to this column.
  • c — classes, attribute selectors, and pseudo-classes. Each .class, [attr=value], or :hover/:nth-child()/etc. adds 1 to this column.
  • d — type selectors and pseudo-elements. Each element type like div or p, and each pseudo-element like ::before, adds 1 to this column.

The universal selector *, combinators (>, +, ~, the descendant space), and :where() contribute zero specificity. The negation pseudo-class :not() and functional pseudo-classes like :is() and :has() contribute the specificity of their most specific argument, not a flat point of their own.

The columns are compared left to right, like digits in different bases — a single ID always beats any number of classes, and a single class always beats any number of type selectors, no matter how many there are. Ten classes cannot outweigh one ID. This is why the tuple notation (0,1,0,0) vs (0,0,10,0) is written with commas rather than as the numbers 100 vs 1000 — there is no carrying between columns.

Syntax

Specificity isn’t a CSS syntax you type yourself — it’s computed automatically from whatever selector you write. But it helps to see all four categories combined in one real selector:

nav#primary-nav .menu-item:hover > a::after {
  content: "→";
}
  • nav and a — two type selectors → column d = 2 (the > combinator itself adds nothing)
  • #primary-nav — one ID selector → column b = 1
  • .menu-item and :hover — one class and one pseudo-class → column c = 2
  • ::after — one pseudo-element → adds to column d, making it 3

The resulting specificity is (0, 1, 2, 3). Any competing rule with a lower first non-zero column loses, regardless of how many selectors it piles on elsewhere.

Examples

Example 1: class beats type

p {
  color: blue;
}

.highlight {
  color: crimson;
}

Applied to <p class="highlight">Hello</p>:

Result: the text renders in crimson, not blue.

The type selector p has specificity (0,0,0,1). The class selector .highlight has specificity (0,0,1,0). Comparing column by column, the class column (c) beats the type column (d) as soon as a is tied at 0 and b is tied at 0 — so .highlight wins even though p appears first and even though p looks like it is targeting the element more directly.

Example 2: an ID-qualified selector beats a lone class

#sidebar .widget-title {
  font-size: 18px;
  color: #333333;
}

.widget-title {
  font-size: 24px;
  color: #0066cc;
}

Applied to <div id="sidebar"><h3 class="widget-title">Recent Posts</h3></div>:

Result: the heading renders at 18px in dark gray (#333333), not 24px blue.

#sidebar .widget-title scores (0,1,1,0) — one ID plus one class. .widget-title alone scores (0,0,1,0). The first rule’s ID column (b=1) beats the second rule’s ID column (b=0), so the first rule wins outright, even though the second rule appears later in the file. This is the single most common source of “I changed the CSS but nothing happened” bugs: a nested ID-qualified rule elsewhere in the stylesheet is quietly out-scoring the class you just edited.

Example 3: !important overrides specificity entirely

.btn {
  background-color: gray;
}

.btn.btn-primary {
  background-color: blue;
}

.btn {
  background-color: green !important;
}

Applied to <button class="btn btn-primary">Submit</button>:

Result: the button renders with a green background, not blue.

Ignoring importance for a moment, .btn.btn-primary at (0,0,2,0) would beat plain .btn at (0,0,1,0). But !important is checked before specificity is ever compared — a normal declaration can never beat an !important one within the same origin, no matter how much more specific its selector is. This is exactly why relying on !important is dangerous: it steps outside the specificity system entirely, and the only way to override it later is with another, even more fragile !important.

How it works step by step

  1. The rendering engine gathers every rule in every stylesheet whose selector matches the element, for the property in question.
  2. It groups matches by origin and importance: user-agent defaults, then author styles, then author !important styles, then user !important styles (highest wins).
  3. Within the winning importance group, it computes the 4-tuple (inline, ids, classes/attrs/pseudo-classes, types/pseudo-elements) for every matching selector.
  4. It compares tuples left to right. The first column where two tuples differ decides the winner; remaining columns are never even considered once a difference is found.
  5. If every column is exactly equal, the rule that was declared later in the source (later in the same file, or in a stylesheet loaded later) wins — this is the cascade’s final tiebreaker.
  6. The winning declaration’s value is used for that property; the browser repeats this per property, so a single element can end up with some properties won by one rule and other properties won by a different rule.

Common Mistakes

Mistake 1: styling with IDs, then fighting them with classes

#nav ul li a {
  color: black;
}

Later, trying to override just the active link:

.nav-link--active {
  color: red;
}

Why it fails: #nav ul li a scores (0,1,0,3) — the ID column alone guarantees it beats any class-only selector like .nav-link--active at (0,0,1,0). The “active” color never shows up, and it looks like the browser is ignoring the new rule.

Corrected: avoid the ID for styling in the first place and keep everything at the class level, so later class rules can actually win by source order:

.nav a {
  color: black;
}

.nav-link--active {
  color: red;
}

Mistake 2: reaching for !important to “just make it work”

.card-title {
  font-size: 20px !important;
}

.card--compact .card-title {
  font-size: 14px !important;
}

Why it’s a problem: both declarations are !important, so the tiebreak falls back to specificity (.card--compact .card-title wins here), but every future developer who needs to adjust font size on a card title now also has to add !important, and the file slowly turns into an arms race that is nearly impossible to unwind.

Corrected: drop !important and let normal specificity (a more specific compound selector for the compact variant) do the job:

.card-title {
  font-size: 20px;
}

.card--compact .card-title {
  font-size: 14px;
}

Best Practices

  • Prefer classes over ID selectors for styling hooks; reserve IDs for JavaScript hooks, fragment links, and form label associations.
  • Keep selectors as flat as possible (e.g. one class, not three nested descendant combinators) — shallow selectors are easier to override predictably and are cheaper for the engine to match.
  • Adopt a consistent naming convention (such as BEM) so most of your selectors end up with the same, low specificity, making overrides about source order rather than specificity arithmetic.
  • Treat !important as a last resort for isolated utility classes (e.g. a .hidden { display: none !important; } helper) or for overriding third-party CSS you cannot edit — never as a routine fix.
  • For large codebases, use @layer to control precedence by layer order instead of by selector complexity — a rule in a later layer beats an earlier layer even with lower specificity, which is often easier to reason about than tuning selectors.
  • When debugging “my CSS isn’t applying,” check the browser DevTools computed/styles panel first — it shows every matching rule and which one won, which is faster than mentally recalculating tuples.

Practice Exercises

  • Given div.card > h2 and .card-heading targeting the same element, calculate each selector’s specificity tuple and state which rule wins.
  • Write a single selector with specificity exactly (0,0,2,0) that could reasonably style a button placed inside an element with class .card.
  • You have #page .sidebar a { color: navy; } and later add .link-external { color: orange; } expecting external links to turn orange, but they stay navy. Explain why, and rewrite the CSS so the override works without using !important.

Summary

  • Specificity is a 4-part score — inline styles, IDs, classes/attributes/pseudo-classes, and types/pseudo-elements — compared left to right with no carrying between columns.
  • !important declarations are resolved before specificity is even checked, so they beat any normal declaration regardless of selector complexity.
  • When specificity is exactly tied, the rule declared later in the source wins.
  • Overusing ID selectors for styling is the most common cause of overrides silently failing.
  • Flat, class-based selectors and a consistent naming convention keep specificity predictable as a project grows, reducing the need for !important.