CSS Specificity
Learn how CSS specificity is calculated using the (A,B,C,D) score system. Understand inline styles, IDs, classes, and elements, and why !important breaks everything.
You’ve been writing CSS for a bit. Things are going fine. Then one day, you change a color, refresh the browser, and nothing happens. You change it again. Still nothing. You start adding more rules. Still nothing. Eventually, in frustration, you slap !important on it, it works, and you move on without knowing why.
That was the specificity system quietly doing its job, and you just didn’t know it existed yet.
This is one of those topics that, once you actually understand it, makes so many past frustrations make complete sense. And it’s not complicated at all once you see the logic behind it.
01A Quick Thread Back to What We’ve Already Covered
If you’ve been following along in this series, you already know a good chunk of what feeds into specificity. In our article on the CSS cascade, we talked about how the browser resolves conflicting styles by looking at three things: origin, specificity, and source order. Specificity was listed there but we kept it brief on purpose, because it deserves its own proper explanation.
We also covered universal, type, class, and ID selectors, and even noted there that ID selectors are “heavier” than class selectors. That weight difference? That’s specificity. Same goes for the pseudo-classes and pseudo-elements we covered: they all carry a specificity score too.
Now we’re putting it all together.
02What CSS Specificity Actually Is
When two CSS rules target the same element and define the same property, the browser needs to pick one. It doesn’t just guess. It runs a scoring system to figure out which selector is more “specific,” and the one with the higher score wins.
That scoring system is CSS specificity.
Think of it like a points system in a game. Every selector earns points based on what types of selectors it uses. A rule with a higher score overrides one with a lower score, regardless of which one came first in your stylesheet.
That last bit is important. Specificity beats source order. If you’ve ever been confused why your later CSS rule wasn’t applying, this is probably the reason.
03The Specificity Score System: How (0,0,0,0) Works
Specificity is represented as four numbers separated by commas: (A, B, C, D). Each number lives in its own column, and each column represents a different “tier” of selector.
You start at zero across the board, and you add points to the relevant column based on what selectors your rule uses.
The Specificity Score Columns
style attribute directly on an HTML element#id identifierColumns are compared left to right. A higher value in column A always beats column B, no matter how large B is.
The columns don’t carry over into each other. (0,1,0,0) always beats (0,0,99,0), even though 99 sounds like a lot more. Each column is its own dimension. Think of them like version numbers: 2.0.0 is always greater than 1.99.99.
Here’s how the hierarchy looks in practice:
Specificity Hierarchy (Highest to Lowest)
04Column A: Inline Styles, the Highest Specificity Score
We covered inline styles back in our inline, internal, and external CSS article. They sit directly on the HTML element using the style attribute. And in the specificity world, they have the highest score of any regular CSS: (1,0,0,0).
This means an inline style will beat any selector-based rule in your stylesheet, no matter how complex that selector is.
<!-- HTML -->
<p id="intro" class="lead-text" style="color: orange;">
What color am I?
</p>
/* CSS */
/* Score: (0,1,1,1) — pretty high! */
#intro.lead-text p {
color: blue;
}
/* Score: (0,1,0,0) */
#intro {
color: green;
}
/* The inline style wins. Color = orange.
Inline style score: (1,0,0,0) */
This is exactly why you want to avoid inline styles for anything you plan to manage from your stylesheet. There’s almost no way to override them from CSS without using !important, which causes its own set of problems (more on that shortly).
Zero specificity to note: The universal selector (*) and CSS combinators like >, +, ~, and the space descendant combinator contribute zero to the specificity score. Only the actual selectors on either side of them matter.
05Column B: ID Selectors and the (0,1,0,0) Weight
Every time you use a #id in a selector, you add 1 to column B. That gives you a score of (0,1,0,0). Since column B is compared before columns C and D, an ID-based selector will always beat a selector made only of classes and elements, no matter how many of those you chain together.
/* Score: (0,0,3,0) — three classes */
.card.featured.highlighted {
background: blue;
}
/* Score: (0,1,0,0) — one ID */
#sidebar {
background: red;
}
/* If the element has both .card.featured.highlighted AND #sidebar,
the background will be red.
(0,1,0,0) beats (0,0,3,0) because column B wins over column C. */
This is why we mentioned in the selectors article that over-relying on ID selectors causes headaches later. They’re heavy hitters in the specificity system, and heavy hitters are hard to override cleanly.
You can have multiple IDs in one selector too. Each one adds another point to column B.
/* Score: (0,2,0,0) — two ID selectors */
#main #sidebar {
color: white;
}
/* Score: (0,1,0,0) — one ID */
#sidebar {
color: gray;
}
/* The two-ID selector wins: (0,2,0,0) vs (0,1,0,0) */
06Column C: Classes, Pseudo-Classes, and Attribute Selectors (0,0,1,0)
This is where most of your day-to-day CSS lives. Every class selector, pseudo-class, and attribute selector you use adds 1 to column C.
So .card gets you (0,0,1,0). Add :hover to it, and you’ve got (0,0,2,0). Chain in an attribute selector like [data-active] and you’re at (0,0,3,0).
/* Score: (0,0,1,0) */
.btn {
color: white;
}
/* Score: (0,0,2,0) */
.btn:hover {
color: yellow;
}
/* Score: (0,0,3,0) */
.btn.primary[disabled] {
color: gray;
}
/* Score: (0,0,4,0) */
.card .btn.primary:hover {
color: orange;
}
The attribute selectors we covered earlier, things like [type="text"], [href^="https"], all fall in column C. Same with all the pseudo-classes: :hover, :focus, :nth-child(), :first-child, etc.
One special case worth knowing: The :not() pseudo-class itself adds zero specificity, but whatever selector you put inside it does count. So :not(.active) adds (0,0,1,0) from the .active class inside, not from :not itself. We’ll see more nuances like this when we get to :is(), :where(), and :has() in the next article.
07Column D: Elements and Pseudo-Elements (0,0,0,1)
The lowest specificity column. Every element type selector (like p, h2, div, span) adds 1 to column D. Pseudo-elements like ::before, ::after, and ::first-letter also go here.
/* Score: (0,0,0,1) — just one element */
p {
color: black;
}
/* Score: (0,0,0,2) — two element selectors */
section p {
color: navy;
}
/* Score: (0,0,0,3) — three elements */
article section p {
color: darkblue;
}
/* Score: (0,0,0,2) — element + pseudo-element */
p::first-line {
font-weight: bold;
}
/* Score: (0,0,1,1) — class + element */
.article p {
color: gray;
/* Beats all of the above because column C has a 1 */
}
Element selectors are the weakest in the scoring system. They’re fine for global defaults, like resetting margins or setting base font sizes. But for anything more specific, you’ll usually want a class in the selector.
08Calculating CSS Specificity in Practice: Real Examples
Alright, theory is good but let’s actually calculate some selectors. This is where it clicks. You just go through the selector piece by piece, count what belongs to each column, and write the score.
/* Let's calculate these selectors step by step */
/* 1. h1
→ 1 element (h1)
Score: (0,0,0,1) */
h1 { color: black; }
/* 2. .nav-link
→ 1 class
Score: (0,0,1,0) */
.nav-link { color: blue; }
/* 3. nav .nav-link
→ 1 element (nav) + 1 class (.nav-link)
Score: (0,0,1,1) */
nav .nav-link { color: blue; }
/* 4. #header nav .nav-link
→ 1 ID + 1 element + 1 class
Score: (0,1,1,1) */
#header nav .nav-link { color: blue; }
/* 5. .card .btn:hover
→ 2 classes + 1 pseudo-class
Score: (0,0,3,0) */
.card .btn:hover { background: green; }
/* 6. div#sidebar.active[data-open]
→ 1 element + 1 ID + 1 class + 1 attribute
Score: (0,1,2,1) */
div#sidebar.active[data-open] { display: block; }
See the pattern? You’re just counting. No tricks, no magic.
Now let’s see what happens when two selectors fight over the same element and property:
Specificity Battles: Which Selector Wins?
B:1
C:1
D:0
B:0
C:2
D:1
B:0
C:1
D:1
B:0
C:3
D:0
B:1
C:1
D:1
B:0
C:3
D:1
That third battle is a great one to think about. Selector B has three classes and an element, which looks pretty heavy. But Selector A has one ID, and that single ID in column B is enough to win, because column B is checked before column C.
09When Two Selectors Have the Same Score: Source Order Decides
If two selectors land on the exact same specificity score, the cascade moves to source order as the tiebreaker. The rule that appears later in the stylesheet wins.
/* Both are (0,0,1,0) — same score */
.btn {
background: blue; /* Declared first */
}
.btn {
background: green; /* Declared later — this wins */
}
This is the “C” in CSS: Cascading. When specificity is equal, things literally cascade, and the last one wins. That’s also why your <link> tag order matters when you have multiple stylesheets: the later stylesheet can override an earlier one if the scores match.
We covered this in detail in the cascade article if you want to revisit it.
10A Live Look at a Specificity Conflict
Here’s a real-world scenario, showing which rule applies and why:
Specificity Conflict: What Color Does This Button Get?
Three rules target the same element. The browser runs the specificity scores and picks the winner.
CSS Rules (in order)
background: blue;
} /* (0,0,0,1) */
background: purple;
} /* (0,0,1,1) */
background: #ec4899;
} /* (0,0,3,1) ✓ Winner */
Result in the Browser
The button gets background: #ec4899
Score (0,0,3,1) wins over (0,0,1,1) and (0,0,0,1)
because column C has the highest value.
11Try the Specificity Calculator
Here’s an interactive tool to calculate the specificity score of any CSS selector. Just type it in and hit Calculate.
Quick note about JavaScript: The calculator below uses JavaScript to process your input. We haven’t covered JavaScript yet in this series, so don’t worry if that sounds unfamiliar. It’ll be explained in full later. For now, just use the tool and focus on understanding the specificity scores it gives you.
CSS Specificity Calculator
Type any CSS selector and see its (A, B, C, D) score instantly.
Try these examples:
.btn.primary
#nav .link:hover
div.card p
.section h2::before
#app .list li.active[aria-selected]
12!important in CSS: Power That Comes at a Price
There’s one more thing that affects which style applies, and it sits completely outside the normal scoring system: !important.
When you add !important to a declaration, it overrides all other declarations for that property on that element, regardless of specificity. It’s the nuclear option.
/* This has a very high score: (0,1,2,1) */
#page .container article p {
color: navy;
}
/* This has a low score (0,0,0,1)... */
p {
color: red !important;
/* ...but !important makes it win anyway */
}
Sounds handy, right? It isn’t, not really. Here’s the honest reason why !important is a red flag in CSS:
Why !important Is a Red Flag
- It breaks the natural flow of the cascade, making your CSS harder to reason about. You can’t just read the selectors to know which style wins anymore.
- The only way to override an
!importantrule is with another!importantrule with equal or higher specificity. You end up in a specificity arms race. - It signals that something in your CSS architecture is fighting itself. The right fix is usually to adjust the selectors, not force a win with
!important. - It makes debugging painful for you and everyone else working on the same codebase. Future-you will not thank past-you for this.
- When you reach for
!important, it usually means a specificity conflict that should be resolved at the selector level, not the declaration level.
Now, there are situations where !important is completely legitimate. If you’re writing a utility class library (like Tailwind’s !important mode), or if you need to override third-party styles that are too specific to beat normally, !important makes sense. The key word is intentional. Using it as a random fix when styles “aren’t working” is the problem.
Also worth knowing: Two !important rules competing against each other fall back to specificity comparison. So even with !important, the regular specificity system still applies as a tiebreaker. A higher-specificity !important rule beats a lower-specificity one. And user stylesheets with !important can override author !important rules, which is a browser accessibility feature.
13Finding Specificity Issues in Browser DevTools
This is genuinely one of the most useful skills you can develop. When a style isn’t applying and you don’t know why, the browser DevTools will tell you exactly what’s happening.
Here’s the workflow:
Right-click the element in your browser and click Inspect. In the Styles panel on the right, you’ll see all the CSS rules that match that element, listed from highest to lowest specificity. Rules that are overridden show up with a strikethrough. You can hover over the selector in DevTools and it’ll often show you the specificity score directly.
<!-- Try this in the browser and open DevTools to see the cascade in action -->
<!DOCTYPE html>
<html>
<head>
<style>
/* Score: (0,0,0,1) */
p { color: black; }
/* Score: (0,0,1,1) */
.text p { color: blue; }
/* Score: (0,1,0,1) */
#main p { color: green; }
</style>
</head>
<body>
<div id="main">
<div class="text">
<p>What color am I? Open DevTools to see which rules apply.</p>
</div>
</div>
</body>
</html>
In DevTools, you’ll see all three rules listed for that p element. The p { color: black; } and .text p { color: blue; } rules will be struck through. The winner, #main p { color: green; }, won’t be.
This is your best debugging tool for specificity conflicts. Use it every time before reaching for !important.
14Writing CSS That Doesn’t Fight Itself: Practical Tips
Understanding specificity is one thing. Writing CSS that avoids specificity wars in the first place is the real goal. Here are five habits that will save you a lot of headaches:
Keep Selectors Flat
Avoid long chains like section div.wrapper article p.lead. A simple .lead-text class does the same job with far less specificity, and it’s easier to override when you need to.
Prefer Classes Over IDs
ID selectors are heavy. Use them for JavaScript targeting or anchor links, not for styling. A class-based system is much easier to manage as the stylesheet grows.
Avoid Inline Styles in HTML
Inline styles lock you into the highest specificity tier. If you need to change that style from your stylesheet later, you almost can’t, without hacking around it.
Treat !important as a Last Resort
Before you add it, ask why the styles are conflicting. Usually the answer is a selector that’s too broad or too specific in the wrong place. Fix the selector instead.
Use DevTools as You Write
Don’t wait until you have a bug. Checking specificity scores in DevTools as you style things builds the habit of writing intentional, low-conflict CSS.
Order Your Stylesheet Intentionally
Put general rules first (elements, base styles), then more specific component styles. This way, source order works with the cascade, not against it.
15Specificity Quick Reference
CSS Specificity Cheat Sheet
| Selector Type | Example | Score Added | Column |
|---|---|---|---|
| Inline style | style="..." |
(1,0,0,0) | A |
| ID selector | #header |
(0,1,0,0) | B |
| Class selector | .btn |
(0,0,1,0) | C |
| Attribute selector | [type="text"] |
(0,0,1,0) | C |
| Pseudo-class | :hover, :nth-child() |
(0,0,1,0) | C |
| Element / type selector | p, div, h1 |
(0,0,0,1) | D |
| Pseudo-element | ::before, ::after |
(0,0,0,1) | D |
| Universal selector | * |
(0,0,0,0) | None |
| Combinators | > + ~ (space) |
(0,0,0,0) | None |
| !important | color: red !important |
Overrides all | Outside cascade |
16What You’ve Actually Learned Today
Let’s be honest, specificity looks intimidating until you see the pattern. Once you do, it’s just counting.
You now know that every CSS selector has a four-column score: (A, B, C, D). Inline styles go in A, IDs in B, classes and pseudo-classes in C, and elements in D. Columns are compared left to right, and the highest score wins. When two scores tie, source order takes over.
You also know that !important skips the whole system entirely, which is why it causes so many problems in large stylesheets. And you know that the right response to a specificity conflict is usually a better selector, not a heavier one.
That’s a solid foundation. Keep it in the back of your head every time you write a selector, and you’ll make much more intentional decisions about how you structure your CSS.
17What’s Coming Up Next
The next article in this series covers three modern CSS selectors that have a special relationship with specificity: :is(), :where(), and :has(). You’ll see how :is() borrows the specificity of its most powerful argument, how :where() lets you group selectors at zero specificity cost, and how :has() gives us something CSS didn’t have for years: a real parent selector.
If you want to sharpen what you’ve learned today, open DevTools on any page you’ve built, inspect an element, and walk through every rule in the Styles panel. Try to calculate the score yourself before reading what DevTools shows you. Ten minutes of that exercise is worth more than any tutorial.
CSS Specificity
Loading challenge…
Ready to Practice?
Select a difficulty level above to load your challenge.