You write a CSS rule, you reload the page, and… nothing changes. The color you set is being ignored. You add color: red, hit refresh, and the text stays stubbornly blue. There’s no typo, the selector matches, and yet the browser flat-out refuses to listen.
Nine times out of ten, the culprit is specificity — the system CSS uses to decide which rule wins when several of them target the same element. Once you understand how it’s scored, those “why won’t this style apply?!” mysteries mostly evaporate. This is one of those topics that separates people who fight CSS from people who feel in control of it, so let’s get it nailed down.
The problem specificity solves
CSS lets you target the same element from many different angles. A single paragraph might be matched by a tag selector, a class selector, an ID, and something inherited from a parent — all at once, each trying to set a different color:
p { color: blue; }
.intro { color: green; }
#first { color: red; }
<p id="first" class="intro">Which color am I?</p>
All three rules match that paragraph. So which color does it actually get? The browser can’t apply all three, so it needs a tie-breaker. That tie-breaker is specificity: a way of scoring how specific each selector is, where the more specific selector wins.
The answer here is red, because an ID is more specific than a class, which is more specific than a tag. But to predict that confidently every time, you need to know how the score is actually calculated.
How specificity is calculated
Think of specificity as a score made of three slots, written like (a, b, c). You count up what’s in your selector and fill the slots:
- a — the number of ID selectors (
#header) - b — the number of class selectors (
.btn), attribute selectors ([type="text"]), and pseudo-classes (:hover) - c — the number of type selectors (
div,p,h1) and pseudo-elements (::before)
Higher slots beat lower ones no matter what’s below. You compare left to right: whoever has more IDs wins outright; if they tie on IDs, you compare classes; if those tie too, you compare type selectors. Here’s how a few selectors score:
p /* (0, 0, 1) — one tag */
.intro /* (0, 1, 0) — one class */
#first /* (1, 0, 0) — one ID */
nav a /* (0, 0, 2) — two tags */
.menu a /* (0, 1, 1) — one class, one tag */
#nav .item a /* (1, 1, 1) — one ID, one class, one tag */
When two rules collide, you read the scores left to right and the bigger number in the leftmost differing slot wins.
The slots don't roll over like normal numbers
Specificity isn’t base-10. A score of (1, 0, 0) beats (0, 12, 0) even though twelve classes feel like more “stuff.” A single ID outranks any number of classes, and a single class outranks any number of tags. The slots never carry over — eleven classes will never add up to one ID. Picture them as three completely separate columns, compared one at a time from the left.
Walking through a real comparison
Let’s settle a genuine conflict. Say you have this markup and these two rules:
<a class="button primary" href="/signup">Sign up</a>
.button { background: gray; } /* (0, 1, 0) */
.button.primary { background: blue; } /* (0, 2, 0) */
Both selectors match the link. The first has one class, scoring (0, 1, 0). The second chains two classes together, scoring (0, 2, 0). Comparing left to right: IDs tie at 0, then we hit the class column — 2 beats 1 — so the second rule wins and the button comes out blue. Chaining selectors like .button.primary is a clean, deliberate way to make one rule more specific than another without reaching for IDs or !important.
When specificity is tied
What happens when two rules have the exact same specificity? Then specificity can’t break the tie, and CSS falls back to source order: the rule that comes last in your stylesheet wins.
.btn { color: white; }
.btn { color: yellow; } /* this one wins — it comes later */
Both are (0, 1, 0), a dead tie, so the later one takes over and the text is yellow. This is why the order of your rules matters, and why a stylesheet you load later can override one loaded earlier when their selectors are equally specific. “Last one wins” is the rule whenever specificity itself is a draw.
This is how the cascade works
The word “cascade” in Cascading Style Sheets refers to exactly this process: the browser gathers every rule that matches an element, then ranks them — first by importance, then by specificity, and finally by source order — to pick a single winner for each property. Specificity is the middle step, and it’s the one people trip over most.
Inline styles and where they sit
There’s one more level above everything we’ve covered: inline styles, the ones written directly on an element with the style attribute.
<p style="color: purple;">I'm styled inline.</p>
An inline style is more specific than any selector you could write in a stylesheet — even an ID selector. In the full specificity model it’s sometimes written as a fourth, leftmost slot: (1, 0, 0, 0). In practice, all you need to remember is that an inline style beats your external CSS, which is exactly why inline styles are so hard to override and generally worth avoiding for anything reusable. If a style “won’t change no matter what,” check whether something is setting it inline.
The !important escape hatch
Sitting above even inline styles is !important. Tack it onto a declaration and that declaration jumps to the top of the priority ladder, ignoring the normal specificity comparison entirely:
.btn { color: white !important; }
#special { color: black; } /* loses, despite the ID */
Here the ID selector would normally win, but !important overrides it, and the button text stays white. It’s a sledgehammer — and that’s precisely the problem.
Treat !important as a last resort
!important works, but it’s a trap. Once you use it to win one fight, the only way to override that later is another !important, and soon your stylesheet is an arms race nobody can reason about. It throws the whole specificity system out the window. Reach for it only when you genuinely can’t change the original CSS — for instance, overriding a third-party library you can’t edit. For your own code, restructure the selectors instead.
The universal selector and combinators add nothing
A couple of things you might expect to count actually don’t. The universal selector (*) and combinators (>, +, ~, and the descendant space) contribute zero to specificity:
* /* (0, 0, 0) — adds nothing */
ul > li /* (0, 0, 2) — the > is free, just two tags */
.menu > .item /* (0, 2, 0) — the > is free, just two classes */
So * is the weakest possible selector — almost anything will override it — and putting a > or + between two selectors doesn’t bump the score; only the actual selectors on either side count. This is handy to know when you’re trying to predict which rule wins and you spot a combinator in the mix.
Keeping specificity low and manageable
Now the practical payoff. The single best habit for sane CSS is to keep specificity low and flat. When most of your rules are single classes — all sitting at (0, 1, 0) — overriding any of them is trivial: just write another single-class rule later. The trouble starts when specificity creeps upward with long selector chains and IDs, because then every override has to be even more specific, and the numbers spiral.
/* Hard to override later — deep and ID-heavy */
#sidebar ul li a.link { color: blue; } /* (1, 1, 2) */
/* Easy to override later — flat and class-based */
.sidebar-link { color: blue; } /* (0, 1, 0) */
Both can style the same link, but the second is far kinder to your future self. A few habits keep things flat:
- Prefer classes over IDs and tag selectors for styling. Classes sit at a comfortable middle level that’s easy to layer.
- Don’t over-qualify.
.btnis better thana.btnornav ul li .btn— extra selectors just inflate the score for no benefit. - Avoid IDs for styling. They’re fine as JavaScript hooks or anchor targets, but their high specificity makes them painful to override in CSS.
- Save
!importantfor emergencies, like wrestling with code you can’t edit.
Keep your specificity low and even, and CSS stops feeling like a fight — overrides just work, and you spend far less time wondering why a style won’t apply.
Wrapping up
Specificity is the browser’s tie-breaker for deciding which CSS rule wins, and now you know how it’s scored:
- The browser counts IDs, then classes (plus attributes and pseudo-classes), then type selectors into three slots
(a, b, c). - Higher slots always beat lower ones — one ID outranks any number of classes; the slots never carry over.
- When specificity is tied, source order breaks it: the last matching rule wins.
- Inline styles beat any selector, and
!importantbeats even those — but!importantis a last resort. - The universal selector
*and combinators (>,+,~) add nothing to the score. - Keep specificity low and flat — lean on single classes — so your CSS stays easy to override.
This builds directly on how selectors work, so if matching elements still feels shaky, it’s worth circling back to CSS selectors and what CSS is first. With specificity under your belt, you’ve got real command over which styles apply — and that’s the difference between guessing and knowing.