CSS

CSS Specificity and the Cascade: Which Rule Wins?

Why isn't my CSS working? Learn how the cascade and specificity decide which rule wins, with a live demo that ranks every rule and explains each loss.

RazakTerra
Reading time
10 min read
Comments
Discussion
CSS Specificity and the Cascade: Which Rule Wins?
On this page
  1. Every rule enters a contest
  2. Watch the contest
  3. Specificity: the score of a selector
  4. A few special cases
  5. Inheritance is not part of the contest
  6. Inline styles
  7. !important
  8. Debugging a losing rule
  9. Keep specificity low and flat
  10. Try it yourself
  11. Wrapping up

Sooner or later, every CSS beginner has the same afternoon. You write a rule. You save. You refresh. Nothing changes. You check the spelling, the semicolons, the file path. Everything’s fine. The rule is right there, and the browser is ignoring it.

It isn’t, though. Almost every time, your rule is working perfectly. It’s just losing. Somewhere else in your CSS, another rule is setting the same property on the same element, and the browser has decided that one should win.

That decision isn’t random, and it isn’t a matter of taste. It’s made by a short, strict list of questions called the cascade, the “C” in CSS. In the last post we learned how selectors pick elements. In this one we’ll learn what happens when several of them pick the same element and disagree. Once you know the questions, “why isn’t my CSS working?” turns into a puzzle you can actually solve.

Every rule enters a contest

Here’s the mental model for this post: every declaration that applies to an element enters a contest for each property, and the cascade asks the contestants the same questions in the same order. The first question that separates two of them decides who wins.

For the CSS you’ll write day to day, there are four questions, asked in this order:

  1. Is one of them !important? An important declaration beats a normal one.
  2. Is one of them an inline style? A style="…" attribute beats any selector.
  3. Which selector is more specific? The higher specificity wins.
  4. Which one comes later? If everything else is equal, the last one written wins.

The order matters as much as the questions. Specificity is only asked about when importance and inline styles didn’t settle it. Order is only asked about when specificity was a tie. A later rule never beats a more specific one, no matter how far down the file it sits.

Watch the contest

Below is one paragraph, <p id="intro" class="lead note">, inside a <div id="page">. There are seven rules in the stylesheet, and every one of them sets color. Only some are switched on to begin with. The ranking on the right lists every rule that selects the paragraph, best first, and says why each one lost to the one above it. Underneath, the figure checks its answer against the colour your browser actually computed.

Which rule wins?
styles.css
  1. #page { color: crimson }(1, 0, 0)
  2. p { color: gray }(0, 0, 1)
  3. .lead { color: teal }(0, 1, 0)
  4. .note { color: hotpink }(0, 1, 0)
  5. p.lead { color: purple }(0, 1, 1)
  6. #intro { color: royalblue }(1, 0, 0)
  7. .lead { color: seagreen !important }(0, 1, 0)
<div id="page"> <p id="intro" class="lead note"></div>
  1. .note { color: hotpink }Wins.
  2. .lead { color: teal }Loses to .note: same specificity, and it comes later.
  3. p { color: gray }Loses to .lead: higher specificity: (0, 1, 0) beats (0, 0, 1).
  4. #page { color: crimson }Not in the contest: it selects the div, not the paragraph. Its colour only reaches the paragraph by inheritance, and anything that selects the paragraph itself wins over that, even a bare p.

The paragraph is hotpink.

Switch rules on and off, move them up and down, or add an inline style. Every rule that selects the paragraph is ranked on the right.

It starts out hotpink. Then try these, one at a time, and watch the ranking reshuffle:

  • Move .note above .lead. The paragraph turns teal. Both selectors are a single class, so their specificity is a tie, (0, 1, 0) each, and the cascade falls through to the last question: whichever comes later wins.
  • Switch on p.lead. Purple. A class plus a type, (0, 1, 1), beats a class on its own. Now the order of .lead and .note doesn’t matter at all, because neither of them is winning.
  • Switch on #intro. Royal blue. One ID beats any number of classes.
  • Add the inline style. Dark orange. A style attribute beats every selector, including the ID.
  • Switch on .lead { … !important }. Sea green, the weakest-looking selector on the list. Importance is the very first question, so it beats everything else here, inline style included.

Now switch off every rule that selects the paragraph, and look at #page { color: crimson }. It has the highest specificity in the whole stylesheet, an ID, and yet while any other rule was on, it never even made the ranking. We’ll come back to why in a moment.

Specificity: the score of a selector

Specificity is the question that settles most real conflicts, so it’s worth learning to work it out by eye. Every selector gets a score of three numbers, written (IDs, classes, types):

  • IDs: each #id adds one to the first number.
  • Classes: each .class, attribute selector like [type="text"], and pseudo-class like :hover adds one to the second.
  • Types: each type selector like p, and pseudo-element like ::before, adds one to the third.

Some pieces don’t count at all: the universal selector *, and the combinators (the space, >, + and ~). They change which elements are selected, but not how strong the selector is.

Type two selectors below, or pick a pair, and see where every point comes from:

Specificity, side by side
#navID␣combinatoratype
1IDs
0Classes, attributes, pseudo-classes
1Types, pseudo-elements
.navclass␣combinator.menuclass␣combinator.itemclass␣combinatoratype
0IDs
3Classes, attributes, pseudo-classes
1Types, pseudo-elements

A wins, decided in the ID column: 1 against 0. The columns to its right never get a say.

Try
Type two selectors, or pick a pair. The columns are compared from the left; the first one that differs decides.

The most important thing the duel shows is how the scores are compared. They are compared column by column, from the left, like version numbers. They are never added up. The first column that differs decides, and the columns to its right never get a say.

That’s why the pair with eleven classes loses to a single ID. (0, 11, 0) against (1, 0, 0): the ID column differs first, and that’s the end of it. Eleven classes can’t “carry over” into the ID column, the way 11 can carry over into the tens place in arithmetic.

A few special cases

Three pseudo-classes take a list of selectors and follow special rules:

  • :is() and :not() count as their most specific argument. :is(#main, .side) a scores (1, 0, 1) even when it matches the .side element, because #main is in the list.
  • :has() works the same way.
  • :where() always counts zero, whatever you put inside it. That makes it perfect for defaults that are meant to be easy to override, which is exactly why modern CSS resets use it.
reset.css
CSS
/* Scores (0, 0, 1): any class will override it. */
:where(ul, ol)[role="list"] li {
list-style: none;
}

Inheritance is not part of the contest

Back to that puzzle: why does #page { color: crimson }, with an ID, lose to a bare p?

Because it was never in the contest. Look at what it selects: the div, not the paragraph. The cascade only compares declarations that apply to the element itself. Crimson reaches the paragraph a different way, by inheritance: a few properties, mostly the ones about text, are passed down from parent to child, but only when nothing selects the child directly.

So any rule at all that targets the paragraph, however weak, beats a value it would have inherited. Specificity doesn’t come into it.

CSS
#sidebar {
color: crimson;
}

/* The paragraphs inside #sidebar are gray, not crimson. */
p {
color: gray;
}

This catches people out constantly. If you’ve ever set a colour on a container with a very specific selector and watched the links inside it ignore you, this is why: the browser’s own stylesheet selects a directly and gives it a colour, and that beats anything inherited from the container.

Inline styles

A style attribute applies to one element only, with no selector at all, and it beats every rule in your stylesheets. That makes inline styles tempting as a quick fix, and painful as a habit: the only thing in a stylesheet that can override one is !important.

The one place they make sense is when JavaScript sets them for values that change all the time, like the position of an element you’re dragging. That’s exactly the job they’re good at.

!important

Adding !important to the end of a declaration moves it into a higher league. It beats every normal declaration, whatever the selector, whatever the order:

CSS
.lead {
color: seagreen !important;
}

When two important declarations compete, the questions start again inside that league: an important inline style beats an important rule in a stylesheet, then specificity decides, then order.

It’s tempting to reach for !important whenever a rule won’t apply. Try not to. Every !important is a rule you can only override with another !important, and stylesheets that rely on it tend to escalate until nothing can be changed without one. If you understand why a rule is losing, there’s almost always a cleaner fix: a slightly more specific selector, or moving a rule further down the file.

There are legitimate uses. A small utility class that must always win, like .visually-hidden, is a common one. So are the user’s own stylesheets: people who need larger text or higher contrast can set their own styles in the browser, and their !important declarations beat yours on purpose. That’s the cascade protecting your readers.

Debugging a losing rule

When a rule doesn’t apply, go through the questions in order, using your browser’s developer tools:

  1. Right-click the element and choose Inspect. The Styles panel lists every rule that selects it, the winners at the top.
  2. Find your declaration. If it’s crossed out, it lost. The rule above it that sets the same property is the one that beat it.
  3. If your rule isn’t listed at all, the problem is the selector, not the cascade: it doesn’t select this element. Check it against the HTML.
  4. If the rule is listed under an Inherited from heading, it selects a parent, not this element, and anything that selects the element directly will beat it.
  5. Compare the two selectors’ specificity. Hover a selector in the Styles panel and most browsers show its score.

Keep specificity low and flat

The easiest cascade to work with is one where most selectors have roughly the same, low specificity. Then the order of your rules does what you’d expect, and overriding something is a matter of writing a rule further down.

  • Style with single classes. .card-title instead of #sidebar .card h2.
  • Avoid IDs in selectors. One ID outweighs any number of classes, which makes it very hard to override later.
  • Don’t nest selectors deeper than you need. Every extra level adds weight you’ll have to match later.
  • Use :where() for defaults you want to be easy to override.

Try it yourself

Here’s a button with a few competing rules. Predict the colour before you look at the preview, then change the order, the selectors and the !important and see if you can predict each result.

Who wins?
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Cascade</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <nav id="main-nav">
    <a class="button primary" href="#">Sign up</a>
  </nav>
</body>
</html>
Result
Cmd/Ctrl + Enter runs it · Esc leaves the editor

Wrapping up

When a rule doesn’t apply, it’s usually not broken. It’s losing a contest it didn’t know it had entered. The cascade asks the same questions every time, and the first one that separates two declarations decides.

  1. Only declarations that select the element itself compete. Inherited values lose to all of them.
  2. !important beats normal declarations.
  3. An inline style attribute beats any selector.
  4. Higher specificity beats lower: (IDs, classes, types), compared column by column from the left, never added up.
  5. If all of that ties, the later rule wins.
  6. * and combinators count zero, :where() counts zero, and :is(), :not() and :has() count as their strongest argument.
  7. Keep specificity low and even, and reach for !important only on purpose.

Next up: the box model. Now that we know which rules win, we’ll look at what one of the most common ones actually does, and why setting width: 200px so often gives you something wider than 200 pixels. See you there! 🏆

Written by

Razak

Writes about the web, slowly, and builds small tools on the side.

Discussion

Comments

Loading comments…

Keep reading