Learn CSS · Lesson 1 of 11 · 10 min
The cascade & specificity
Widely availablesince Sept 2024 Newly availablesince Mar 2026
Two rules set the same property on the same element. One says blue, one says red, and only one can win. The cascade is the procedure the browser follows to pick the winner. It is six tests run in a fixed order, and it stops at the first test that separates the two rules. Knowing the order is the difference between fixing a style and fighting it.
One property at a time
The cascade does not pick a winning rule. It picks a winning declaration, separately
for every property. If one rule sets color and another sets font-size, both
apply and nothing was contested. A conflict exists only when two declarations set the same property on
the same element, and a rule can win color while losing padding.
The six tests, in order
- Origin and importance. Where the rule comes from: the browser’s defaults, the user’s settings, or your stylesheet. Yours beats the browser’s. Adding !important moves a declaration into a separate, higher group, and inside that group the order of origins is reversed.
- Inline styles. A style attribute on the element beats every selector in your stylesheets, whatever they score. This is why styles set from JavaScript are so hard to override from CSS.
- Layers. Widely available If your rules are in cascade layers, the layer declared later wins, and specificity is not consulted at all. Rules outside any layer beat all layered rules.
- Specificity. Inside the same layer, the more specific selector wins. This decides most everyday conflicts, and the next section is about it.
- Scope proximity. Newly available If two rules are tied and both come from @scope blocks, the one whose scope root is fewer steps up the tree from the element wins.
- Source order. Still tied: the declaration that appears last wins.
Read the list again with one question in mind: where is specificity? Fourth. Two whole tests can settle a conflict before the browser looks at your selector at all, and one of them, layers, is a thing you control. (Shadow DOM adds one more test, between the first two. Unless you write web components you will not meet it.)
How specificity is scored
Every selector gets three numbers, written (a, b, c) and compared left to right like a
version number:
- a counts IDs:
#header. - b counts classes, attribute selectors and pseudo-classes:
.btn,[type="text"],:hover. - c counts element names and pseudo-elements:
p,::before.
The columns never carry over. Eleven classes score (0, 11, 0) and still lose to a single
ID at (1, 0, 0). The universal selector * and the combinators
(>, +, ~, a space) add nothing.
p (0, 0, 1)
.note (0, 1, 0)
.menu a (0, 1, 1)
a.menu:hover (0, 2, 1)
#hero .title (1, 1, 0) strong(0, 0, 1)tealoverridden.word(0, 1, 0)orangeoverridden#target(1, 0, 0)indigowinnerThe selected word is indigo.
Four pseudo-classes take a selector list as an argument, and they do not all score alike:
:is(),:not()and:has()score as their most specific argument.:is(#hero, p)is(1, 0, 0)even when it is matching a plain paragraph.:where()always scores zero, whatever is inside it.
Paste any selectors into the specificity calculator and it ranks them by these rules.
Layers: choose the winner before specificity is asked
A cascade layer is a named group of rules. You declare the order of the groups once, at the top, and from then on a rule in a later layer beats any rule in an earlier one, however specific the loser is. The three rules below all target the same button. Reorder the layers and watch the one with the weakest selector win.
Your browser does not support cascade layers, so it ignores every layered rule here and the button keeps its default look.
Two results in there surprise people. A rule outside every layer beats all of them, so
layers are safe to adopt gradually: old unlayered CSS keeps winning until you move it in. And
!important reverses the layer order: among important declarations the
first layer wins. That is deliberate. It lets a reset layer say “this one really is not
negotiable” and have it hold against everything declared after it.
Four tools for keeping specificity low
Put third-party CSS underneath yours
Widely available@layer reset, vendor, components, utilities;
@import url("carousel.css") layer(vendor);
@layer components {
.carousel-arrow { background: black; }
} Goes: the order statement first in your first stylesheet. It is the only line that decides the order.
The library can use any selectors it likes. Your one-class rule still wins, because
components comes after vendor. Three to five layers is plenty for a whole site.
What it replaced: copying the library’s long selector and adding one more class to it, or !important.
Write defaults that anything can override
Widely available:where(ul, ol)[role="list"] { padding: 0; }
:where(.prose) a { color: var(--link); } Goes: around the part of a selector you want to cost nothing.
The first selector scores (0, 1, 0) for the attribute and nothing for the element
names. The second scores (0, 0, 1), so a single class on a link beats it. Use it for
resets and base styles, the rules whose whole job is to lose politely.
What it replaced: base styles that had to be out-specified forever after.
Know what nesting does to the score
Widely available.card, #promo {
& p { color: grey; } /* same as :is(.card, #promo) p */
}
/* (1, 0, 1) for every match, including a p inside .card */ Goes: nowhere. This is a trap to recognise, not a tool to use.
The nesting selector & behaves like :is() wrapped around the parent
selector list, so it takes the score of the most specific item in that list. One ID in a
comma list raises every nested rule underneath it. Keep IDs out of lists you nest inside.
Careful. This is different from Sass, which expands each parent separately. CSS that was converted from Sass nesting can change which rule wins.
Style a component without naming every part
Newly availableI am in the card, so I am styled.
I am inside the slot. The scope stopped before it reached me.
@scope (.card) to (.card-slot) {
p { color: var(--muted); } /* stops at the slot */
} Your browser does not support this yet, so the demo above shows the fallback. Give the scoped elements a usable default colour outside the @scope block. A browser without @scope ignores the block and both paragraphs above read the same, which is plain but correct.
Goes: around the rules for one component.
Inside the block you can write p and img, short selectors with low scores,
and they cannot match anything outside the card or below the boundary. When two scoped rules tie,
the nearer root wins, which is test five. Toggle both boundaries yourself in the
@scope playground.
What it replaced: long class names whose only purpose was to be unique.
Practice
- In the layer widget, make the grey reset rule win without touching the order. There is exactly one way. Then explain to yourself why it works.
- Paste
:is(#app, .page) .btnand:where(#app, .page) .btninto the specificity calculator. Same elements matched, different scores. - Find an
!importantin a project of yours. Work out which of the six tests the two rules were tied on, and whether a layer or a:where()would have settled it earlier.
Check yourself
A rule in @layer utilities uses one class. A rule in @layer components uses an ID. Utilities is declared later. Which wins?
The one-class utility. Layers are test three and specificity is test four, so the ID is never compared.
You add cascade layers to an old site, and the old CSS suddenly beats all your new layered rules. Why?
The old CSS is outside every layer, and unlayered rules beat layered ones. Move it into a layer,
for example with @import url("old.css") layer(legacy), and declare that layer first.
What is the specificity of .card :is(h2, #title)?
(1, 1, 0). :is() takes the score of its most specific argument, the ID,
even when the element it matched was the h2. With :where() it would be
(0, 1, 0).
The mental model to keep
The cascade is a list of tie-breakers, and you get to choose which one settles your conflicts. Settle them with specificity and every override has to be louder than the last. Settle them with layers and source order and every rule can stay one class long. Low, flat specificity inside a small number of ordered layers is a stylesheet that stays easy to change. Next: inheritance, which is what happens when no rule sets a property at all.
Last reviewed 2026-09-18. Browser status on this page is read from the web-features dataset when the site is built, not typed.