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

  1. 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.
  2. 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.
  3. 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.
  4. Specificity. Inside the same layer, the more specific selector wins. This decides most everyday conflicts, and the next section is about it.
  5. 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.
  6. 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)indigowinner
renders as

The selected word is indigo.

Three rules, one word, all in the same layer. The ID wins, and would win wherever it was written.

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.

    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

    1

    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.

    2

    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.

    3

    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.

    4

    Style a component without naming every part

    Newly available

    I 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 */
    }
    A rule that starts at the card and stops at the slot. Full reference and who ships it →

    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

    1. 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.
    2. Paste :is(#app, .page) .btn and :where(#app, .page) .btn into the specificity calculator. Same elements matched, different scores.
    3. Find an !important in 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.