CSS Specificity: How the Cascade Picks the Winning Rule

Published Updated

Specificity is the score the browser gives each selector to decide which rule wins when several of them set the same property on the same element.

Syntax

/* the browser reads each selector as three tiers: */
/* (ids, classes/attributes/pseudo-classes, types/pseudo-elements) */

#main            /* (1, 0, 0) */
.card .title     /* (0, 2, 0) */
a:hover          /* (0, 1, 1) */
ul li            /* (0, 0, 2) */

The Three Tiers

The browser counts your selector across three buckets and compares them from left to right. An id is worth one point in the first bucket, a class or attribute or pseudo-class is worth one in the second, and a tag or pseudo-element is worth one in the third:

#promo {        /* (1, 0, 0) */
  color: red;
}

.banner .text { /* (0, 2, 0) */
  color: blue;
}

A single id beats any number of classes, because the first bucket is compared before the second one even matters. So #promo wins the color here despite .banner .text carrying two classes. The comparison never adds the buckets together into one number, it walks them in order until one selector pulls ahead.

Inline styles and the universal selector sit outside this same three-tier count. An inline style attribute on an element beats every normal declaration in your stylesheet regardless of score, though a rule marked !important can still override it, and that near-total precedence is exactly why component libraries avoid inline styles for anything meant to be themeable. The universal selector, *, sits at the opposite extreme and scores (0, 0, 0), so it loses to nearly everything.

Source Order Breaks Ties

When two selectors land on the exact same score, the cascade falls back to source order and the rule written later wins. This is why the position of a rule in your stylesheet matters once specificity is equal:

.button {
  background: gray;
}

.button {
  background: green;
}

Both selectors score (0, 1, 0), so the second one wins and the button comes out green. Load order across files counts too, which is how a stylesheet linked later can quietly override an earlier one without any change in specificity.

Source order applies across every stylesheet the page loads, spanning all of them as a single sequence. If your build tool concatenates or bundles files in a different order than you expect, two selectors that tie on specificity can swap which one wins without a single line of CSS changing. That is a common cause of a rule that works locally but flips in production.

The Gotcha: !Important Escapes the Normal Scoring

Adding !important to a declaration lifts it out of the ordinary specificity contest, and only another !important declaration with an equal or higher score can override it. That sounds handy right up until two !important rules collide and you are stuck debugging a fight you started:

.alert {
  color: black !important;
}

#urgent .alert {
  color: red; /* loses, even with an id, because the other rule is !important */
}

The #urgent .alert rule has a higher raw score, yet it still loses because the other declaration is marked important. When you find yourself stacking !important to win, slow down and fix the selector or the load order instead. The selector basics guide covers why leaning on ids creates this kind of trap in the first place.

!important still respects specificity and source order among other !important declarations, it just moves the whole contest into its own separate bracket first. That is why the fix for an !important collision is the same fix as any other specificity problem: figure out which declaration the cascade is actually choosing, then adjust the losing selector or remove the !important you no longer need.

Selectors That Cost Nothing

Modern CSS gives you a way to match elements without paying for them, and it is the cleanest answer to most specificity problems. The :where() pseudo-class always scores (0, 0, 0), no matter what you put inside it:

:where(#sidebar, .widget) a {  /* (0, 0, 1) — only the a counts */
  color: navy;
}

#sidebar a {                   /* (1, 0, 1) */
  color: teal;
}

The first rule matches exactly the same elements it would without :where(), and contributes nothing to the score, so anything you write later overrides it with a plain class. That makes it the right tool for defaults and resets: styles you want applied but never want to fight later.

Its sibling :is() behaves differently and the difference catches people out. :is() takes the specificity of the most specific selector inside it, so :is(#sidebar, .widget) scores as an id even when it matched on the class. The same rule applies to :not() and :has(). Reach for :is() when you want the grouping to be concise, and :where() when you want the grouping to be weak.

Cascade Layers Outrank Specificity

Specificity only settles a contest between declarations that reach that stage, and cascade layers are decided first. For ordinary declarations, a rule in a later layer beats a rule in an earlier one whatever their scores:

@layer base, theme;

@layer base {
  #sidebar .widget a { color: navy; }  /* (1, 1, 1) */
}

@layer theme {
  a { color: teal; }                   /* (0, 0, 1) — wins */
}

The plain a selector wins because theme is declared after base, and the layer comparison happens before either score is looked at. Declarations outside any layer are stronger still, which is worth knowing before you wrap an existing stylesheet in a layer and watch its precedence change.

All of that describes normal declarations, and !important turns it upside down. Among important declarations the layer order reverses, so an important rule in an earlier layer wins, and important rules inside a layer outrank important rules outside every layer. The practical reading is that layers give you a deliberate ordering for your normal styles and a mirror image of it for your emergencies, which is another reason not to reach for !important to settle a layered stylesheet. The CSS guide sets out the full order the cascade works through.

Between them, these two features replace most of what !important used to be needed for. When a rule refuses to apply, the useful question is no longer "how do I score higher" but "which stage is this being decided at", because adding weight to a selector cannot win an argument that was settled a stage earlier.

Example

#sidebar .widget a { /* (1, 1, 1) */
  color: navy;
}

.widget a {          /* (0, 1, 1) */
  color: teal;
}
<aside id="sidebar">
  <div class="widget"><a href="/">Home</a></div>
</aside>

The link comes out navy because the first selector carries an id and scores (1, 1, 1), which beats the id-free (0, 1, 1) of the second rule. Drop the #sidebar from that first selector and both rules tie, at which point source order takes over and the later one wins. Once you can read those three numbers off a selector, the cascade stops feeling random.

Frequently Asked Questions

Does the order of class names in the HTML attribute change which rule wins?

No. The class attribute is an unordered set, so listing one class after another gives it no advantage. What decides the winner is the stylesheet: specificity first, then which rule appears later. Reordering the markup changes nothing.

Do the cascade layers change how specificity works?

They change what is compared first. Layer order is resolved before specificity, so a rule in a later layer wins over a more specific rule in an earlier one. This is the intended way to keep framework and override styles apart.

Does the :where() pseudo-class affect specificity?

No, and that is its purpose. :where() always contributes zero, so a rule written with it stays easy to override. Its counterpart :is() takes the specificity of its most specific argument instead.

How do you override a style you cannot edit?

Work up the ladder: match the specificity and come later in the cascade, raise specificity deliberately by adding a class you control, or place the override in a later cascade layer. Each step keeps the result predictable for whoever needs to override it next.

Is specificity calculated per rule or per declaration?

Per selector, and it is resolved separately for each property. Two rules can each win different properties on the same element, which is why an element sometimes takes its colour from one rule and its padding from another.

Sources

  1. [1]
    Specificity
    (developer.mozilla.org)
  2. [2]
    Introducing the CSS Cascade
    (developer.mozilla.org)
  3. [3]
    ID selectors
    (developer.mozilla.org)