CSS Specificity Explained: How the Cascade Decides Which Style Wins

- CSS Specificity, in One Sentence
- How the Score Is Calculated
- The Specificity Table You Can Bookmark
- Worked Examples: Which Rule Wins?
- !important and Inline Styles: Outside the Normal Game
- Why Your CSS Isn't Applying (The Real Reasons)
- Modern Selectors That Change the Math
- Cascade Layers: Specificity's New Boss
- Keeping Specificity Low and Sane
CSS Specificity, in One Sentence
When two CSS rules set the same property on the same element, specificity is the scoring system the browser uses to decide which rule wins. Every selector earns a score from three columns — IDs, classes, and element types. The rule with the higher score applies. When scores tie, the rule written later in the source wins. That single mechanism explains almost every "why won't this style apply?" moment you have ever had.
Specificity is one of the three questions the cascade asks, alongside origin/importance and source order. If you are still shaky on how CSS decides anything at all, start with what CSS actually is and come back — this guide assumes you know how a rule is structured.
How the Score Is Calculated
Think of every selector's specificity as three numbers, written left to right, most powerful first:
(IDs, classes, elements)
- Column 1 — IDs: each
#idin the selector adds 1. - Column 2 — classes, attributes, and pseudo-classes: each
.class,[attr], or:hover/:focus/:nth-child()adds 1. - Column 3 — element types and pseudo-elements: each tag name (
div,a,li) and::before/::afteradds 1.
You compare left to right. A single ID beats any number of classes, and a single class beats any number of element selectors. The columns do not carry over — 11 classes is (0, 11, 0), which still loses to one ID at (1, 0, 0). It is not base-10 math; it is column-by-column comparison.
What counts for nothing
Three things add zero specificity but often confuse people:
- The universal selector
* - Combinators —
>,+,~, and the descendant space - The
:where()pseudo-class (more on this powerful escape hatch below)
So * {}, ul > li {} — the > adds nothing, but the two element names still score (0, 0, 2).
The Specificity Table You Can Bookmark
| Selector | IDs | Classes | Elements | Score |
|---|---|---|---|---|
* |
0 | 0 | 0 | (0,0,0) |
li |
0 | 0 | 1 | (0,0,1) |
ul li |
0 | 0 | 2 | (0,0,2) |
.nav |
0 | 1 | 0 | (0,1,0) |
a:hover |
0 | 1 | 1 | (0,1,1) |
input[type="text"] |
0 | 1 | 1 | (0,1,1) |
.nav .link.active |
0 | 3 | 0 | (0,3,0) |
#header |
1 | 0 | 0 | (1,0,0) |
#header .nav a |
1 | 1 | 1 | (1,1,1) |
style="..." (inline) |
— | — | — | wins over all selectors |
any rule with !important |
— | — | — | wins over all non-important rules |
Read that table top to bottom and you have the whole hierarchy: elements < classes/attributes/pseudo-classes < IDs < inline styles < !important.
Worked Examples: Which Rule Wins?
Say you have this markup:
<a id="cta" class="button primary" href="/signup">Sign up</a>
And these four rules, all setting color:
a { color: blue; } /* (0,0,1) */
.button { color: green; } /* (0,1,0) */
.button.primary { color: teal; } /* (0,2,0) */
#cta { color: red; } /* (1,0,0) */
The link renders red. #cta scores (1, 0, 0), and that single ID outranks every class-based rule no matter how many classes you stack. Delete the #cta rule and the winner becomes teal — .button.primary at (0, 2, 0) beats .button at (0, 1, 0).
The tie-breaker: source order
When two selectors have the identical score, the last one in the stylesheet wins. This is why order matters:
.button { color: green; }
.button { color: orange; } /* this one wins — same score, written later */
Both are (0, 1, 0). Nothing separates them but position, so the later declaration takes effect. This is also why linking your own stylesheet after a framework like Bootstrap lets your same-specificity overrides land.
!important and Inline Styles: Outside the Normal Game
Two things sit above the ordinary three-column score.
Inline styles — a style="color: purple" attribute on the element — behave as if they have a specificity higher than any selector. In the older four-column model this was written (1, 0, 0, 0). Practically: an inline style beats any rule in your stylesheet that targets the same property, unless that rule uses !important.
!important is the true trump card. A declaration flagged important is lifted into a separate, higher-priority tier of the cascade:
.button { color: red !important; }
#cta { color: blue; } /* loses — the important red wins */
Even though #cta has a higher raw score, !important moves .button into a layer that outranks all normal declarations. When two important rules collide, the browser falls back to comparing their specificity normally.
Use
!importantas a last resort, not a habit. Every important you add is a rule that can only be beaten by another important, and specificity wars spiral fast. Reach for it to override third-party CSS you cannot edit, or for genuine utility overrides — not to patch your own cascade.
Why Your CSS Isn't Applying (The Real Reasons)
"Why is my CSS not applying?" is almost always one of these five, roughly in order of how often they bite:
- A more specific rule is overriding you. Open DevTools, inspect the element, and look at the Styles panel — the losing rule appears with a strikethrough. That is the browser telling you exactly what won.
- Two rules tie and yours comes first. Move your rule later or increase its specificity by one class.
- An
!importantupstream is beating your normal rule. Match it deliberately or, better, remove the upstream important. - The selector doesn't match what you think — a typo, wrong class name, or an element that isn't actually a descendant.
- The property is inherited or invalid, so nothing is "winning" at all — the value you wrote was never parsed.
DevTools is the fastest specificity debugger you own. The struck-through declarations and the top-to-bottom order in the Styles panel are the cascade, rendered visually.
Modern Selectors That Change the Math
Three newer pseudo-classes give you direct control over specificity. All three are supported in every current browser (Chrome, Edge, Firefox, and Safari).
:where() — zero specificity on purpose
:where() always contributes (0, 0, 0), no matter what you put inside it. This is the modern tool for writing defaults that are trivially easy to override:
:where(.card) a {
color: rebeccapurple; /* specificity (0,0,1) — just the `a` counts */
}
Because the .card scope adds nothing, any later .card a { color: ... } rule at (0, 1, 1) wins effortlessly. Design-system and reset authors lean on :where() heavily so their base styles never fight application code.
:is() and :not() — take the highest argument
:is() and :not() adopt the specificity of their most specific argument:
:is(#main, .content) p { } /* the #main pushes this to (1,0,1) */
Even if the element matched via .content, the whole selector scores as if the ID applied. :not(.hidden) scores like .hidden — one class. Knowing this stops :is() from silently inflating your specificity.
| Pseudo-class | Specificity contribution |
|---|---|
:where(anything) |
always (0,0,0) |
:is(#id, .class) |
the most specific argument — here (1,0,0) |
:not(.class) |
the most specific argument — here (0,1,0) |
Cascade Layers: Specificity's New Boss
@layer (supported in all current browsers) changes the order of operations. Rules in a later-declared layer beat rules in an earlier layer regardless of specificity:
@layer reset, base, components;
@layer base {
#sidebar a { color: black; } /* (1,0,0) — but lower layer */
}
@layer components {
.link { color: blue; } /* (0,1,0) — wins anyway */
}
Here .link wins even though #sidebar a has a far higher raw score, because components is declared after base. Layers let you retire ID-versus-class specificity battles entirely by deciding priority at the layer level instead. Unlayered styles beat all layered styles, which keeps the model predictable.
Keeping Specificity Low and Sane
The teams with the fewest cascade headaches follow a handful of habits:
- Prefer classes. A flat world of single-class selectors — the spirit behind BEM naming — means almost everything scores
(0, 1, 0)and source order settles ties predictably. - Avoid IDs for styling. Reserve
idfor JavaScript hooks and anchors; its(1, 0, 0)weight is hard to override on purpose. - Don't over-nest.
.header .nav ul li ais fragile and heavy. If you write Sass, watch your nesting depth — it is the single easiest way to accidentally manufacture high-specificity selectors. Custom properties help here too; see our guide to CSS variables done right for theming without specificity creep. - Use
:where()for resets and defaults so base styles never block later overrides. - Adopt
@layeron larger projects to make priority explicit instead of emergent.
Specificity is not really about memorizing a formula — it is about writing selectors flat enough that you rarely have to calculate anything. Once your rules mostly live in the same column, the cascade becomes boringly predictable, which is exactly what you want. From here, the natural next stop is how those winning rules actually size and space your elements — the CSS box model picks up right where specificity leaves off.