CSSPublished 7/26/2023

CSS Specificity and the Cascade: Who Actually Wins?

A walkthrough of the order the browser uses to resolve style conflicts — how specificity is calculated, the three layers of the cascade, and whether you should ever reach for !important.

One of the most maddening moments in CSS: you write color: red, and the text just refuses to change. You pop open DevTools, and there's your rule with a line through it — some other rule is overriding it.

The instinctive fix is to slap on an !important and move on. But that just kicks the problem down the road — the next conflict will be even harder to untangle. To actually understand which rule wins, you need to understand two mechanisms: the cascade and specificity. By the end of this post, you'll know exactly what order the browser follows to pick a winning rule, and you'll be able to calculate a selector's specificity yourself.

What is the cascade? The browser's decision order

"Cascade" literally means water flowing down in layers, each one stacking on or overriding the one before it. When multiple rules target the same property on the same element, the browser doesn't pick one at random — it runs through a fixed sequence of tiebreakers until exactly one rule is left standing.

That sequence, before @layer enters the picture (more on where layers fit in later), is:

  1. Origin and importance
  2. Specificity
  3. Order of appearance

In other words, the browser first checks where a rule comes from and how important it is. If that's a tie, it moves on to specificity — how precise the selector is. If that's still a tie, the last rule to appear in the source wins.

Round one: origin and importance

Not all CSS comes from you. Browsers ship default styles, users can apply their own overrides, and then there's the CSS you actually write. Stacked together, from lowest to highest priority:

  1. User-agent styles (browser defaults)
  2. User styles
  3. Author styles — the CSS you write
  4. Author styles with !important
  5. User styles with !important
  6. User-agent styles with !important (rare — mostly forced browser settings)

In everyday work, you're almost always operating in the "author styles" layer. Browser defaults sit at the bottom of the priority list, which is why a bare <button> still looks like a native button until you write a single rule that overrides it — at which point your author style wins automatically.

!important is the real wildcard in this round: slap it on a declaration and it jumps straight to the top, beating every rule without !important, regardless of what specificity or source order say.

css
p {
  color: blue !important;
}
 
p {
  color: red;
}

The second rule here comes later and uses the same selector, so you'd expect it to win. But because the first rule has !important, the text stays blue. This is exactly why !important feels great in the moment and terrible for teams — once someone fires the first shot, everyone else has to stack their own !important just to override it, until nobody can tell which one actually sits on top.

Round two: specificity — the more precise your selector, the higher it scores

If !important isn't in play, round one is usually a quick tie (everything's an author style), so specificity takes over. Think of it as a scoring system: what your selector is made of determines its score, and the higher score wins.

The common approach is to break a selector into three tiers, written as an (a, b, c) triple:

  • a: ID selectors, like #header
  • b: classes, attribute selectors, pseudo-classes, like .nav, [type="text"], :hover
  • c: element selectors and pseudo-elements, like div, p, ::before

Compare a first — whichever selector has a higher a wins outright. Only if a ties do you move to b, and only if b ties do you move to c. This isn't three numbers summed into one score — it's ranked like a competition, tier by tier. No matter how much bigger b or c is, a win at a settles it immediately.

Let's run through a few examples:

css
/* (0, 0, 1) */
p {
  color: black;
}
 
/* (0, 1, 0) */
.text {
  color: blue;
}
 
/* (0, 1, 1) */
p.text {
  color: green;
}
 
/* (1, 0, 0) */
#main {
  color: red;
}
 
/* (0, 2, 1) */
.container p.text {
  color: orange;
}

Notice .container p.text: two classes plus an element selector gives it b = 2, which beats a lone .text at (0, 1, 0). So even when both rules target the same element, the compound selector wins. This is also why deeply nested selectors tend to carry more weight — and why they're harder to override later.

Two more things worth knowing:

  • Inline styles (the style attribute right on an HTML element) outrank every selector — think of it as an even higher tier that beats all ID selectors.
  • The universal selector * and combinators >, +, ~ contribute nothing — they're effectively (0, 0, 0). But selectors inside :not(), :is(), and :has() count based on whichever argument scores highest inside the parentheses — the whole thing isn't zeroed out.
html
<p id="title" class="text" style="color: purple;">Hello</p>

Even if an external stylesheet has #title { color: red; }, the inline style outranks it — the text stays purple.

Round three: order of appearance — the tiebreaker of last resort

If origin and specificity both end in a dead tie, the browser falls back to source order — whichever rule appears later wins. This is exactly why the order you import stylesheets or write <style> tags matters, especially with third-party UI libraries: if your override comes before the library's CSS in the source, and your selector isn't more specific, your override silently loses.

css
.btn {
  background: gray;
}
 
.btn {
  background: skyblue;
}

Both rules have identical specificity — (0, 1, 0) — so it comes down to source order. skyblue wins because it's declared last.

Taking control of the cascade with @layer

Modern CSS gives you a way to skip the guessing game entirely: cascade layers, via @layer. You declare a set of layers up front, then assign styles to each one:

css
@layer reset, base, components, utilities;
 
@layer reset {
  * {
    margin: 0;
  }
}
 
@layer components {
  .btn {
    background: skyblue;
  }
}
 
@layer utilities {
  .bg-gray {
    background: gray;
  }
}

When comparing across layers, what matters is the order the layers were declared in — a layer declared later beats one declared earlier, and this rule overrides specificity entirely. That means even if a selector in utilities has lower specificity than one in components, utilities still wins as long as it was declared afterward. This lets a team enforce rules like "reset always loses to base, base always loses to components" explicitly, instead of relying on piling up classes or abusing !important to win the specificity race.

One catch: any plain style that isn't assigned to a layer outranks every named layer — a trap that's easy to hit when you're mixing layered and unlayered CSS.

Common pitfalls: don't treat !important and ID selectors as your emergency fix

Once you understand the full comparison order, two common bad habits become obvious:

  • Writing styles with ID selectors: an ID's specificity is so high that once an element's style is set via #id, overriding it with a class is nearly impossible — you're forced to reach for another ID, or even !important, and the specificity just keeps escalating with every fix.
  • Sprinkling !important everywhere: it skips the entire specificity round. It looks like it solves the problem in the short term, but it turns what should be a predictable priority system into an arms race over who stacks more !importants.

A steadier approach is to write most styles with classes, keeping specificity in a narrow band (mostly somewhere between (0, 1, 0) and (0, 1, 1)), and resolve overrides by adjusting selector structure or source order instead. Save !important for genuine edge cases — like overriding inline styles injected by a third-party library that you can't touch directly.

Wrapping up

When styles conflict, the browser doesn't pick a rule at random — it works through origin and importance, specificity, and order of appearance, one round at a time, until exactly one rule is left. Once you have that order in your head, the next time a style refuses to apply, you can check it against these three rounds: is it getting overridden by !important? Is the selector not specific enough? Or did it simply lose on source order? Once you understand these rules, you won't need to guess your way through style conflicts — or reach for !important as a crutch — ever again.