TH The Sass Way
CSS Fundamentals

CSS Specificity Explained: How the Cascade Decides Which Style Wins

CSS Specificity Explained: How the Cascade Decides Which Style Wins
tldrCSS specificity is the scoring system browsers use to decide which rule wins when several target the same element. Each selector scores three columns: IDs, then classes/attributes/pseudo-classes, then element types. Compared left to right, a higher column always wins, so one ID beats any number of classes. When scores tie, the rule written later in the source applies.

CSS Specificity, in One Sentence

When two CSS rules set the same property on the same element, specificity is the scoring system the browser uses to decide which rule wins. Every selector earns a score from three columns — IDs, classes, and element types. The rule with the higher score applies. When scores tie, the rule written later in the source wins. That single mechanism explains almost every "why won't this style apply?" moment you have ever had.

Specificity is one of the three questions the cascade asks, alongside origin/importance and source order. If you are still shaky on how CSS decides anything at all, start with what CSS actually is and come back — this guide assumes you know how a rule is structured.

How the Score Is Calculated

Think of every selector's specificity as three numbers, written left to right, most powerful first:

(IDs, classes, elements)

You compare left to right. A single ID beats any number of classes, and a single class beats any number of element selectors. The columns do not carry over — 11 classes is (0, 11, 0), which still loses to one ID at (1, 0, 0). It is not base-10 math; it is column-by-column comparison.

What counts for nothing

Three things add zero specificity but often confuse people:

So * {}, ul > li {} — the > adds nothing, but the two element names still score (0, 0, 2).

The Specificity Table You Can Bookmark

Selector IDs Classes Elements Score
* 0 0 0 (0,0,0)
li 0 0 1 (0,0,1)
ul li 0 0 2 (0,0,2)
.nav 0 1 0 (0,1,0)
a:hover 0 1 1 (0,1,1)
input[type="text"] 0 1 1 (0,1,1)
.nav .link.active 0 3 0 (0,3,0)
#header 1 0 0 (1,0,0)
#header .nav a 1 1 1 (1,1,1)
style="..." (inline) wins over all selectors
any rule with !important wins over all non-important rules

Read that table top to bottom and you have the whole hierarchy: elements < classes/attributes/pseudo-classes < IDs < inline styles < !important.

Worked Examples: Which Rule Wins?

Say you have this markup:

<a id="cta" class="button primary" href="/signup">Sign up</a>

And these four rules, all setting color:

a                     { color: blue; }   /* (0,0,1) */
.button               { color: green; }  /* (0,1,0) */
.button.primary       { color: teal; }   /* (0,2,0) */
#cta                  { color: red; }    /* (1,0,0) */

The link renders red. #cta scores (1, 0, 0), and that single ID outranks every class-based rule no matter how many classes you stack. Delete the #cta rule and the winner becomes teal.button.primary at (0, 2, 0) beats .button at (0, 1, 0).

The tie-breaker: source order

When two selectors have the identical score, the last one in the stylesheet wins. This is why order matters:

.button { color: green; }
.button { color: orange; }  /* this one wins — same score, written later */

Both are (0, 1, 0). Nothing separates them but position, so the later declaration takes effect. This is also why linking your own stylesheet after a framework like Bootstrap lets your same-specificity overrides land.

!important and Inline Styles: Outside the Normal Game

Two things sit above the ordinary three-column score.

Inline styles — a style="color: purple" attribute on the element — behave as if they have a specificity higher than any selector. In the older four-column model this was written (1, 0, 0, 0). Practically: an inline style beats any rule in your stylesheet that targets the same property, unless that rule uses !important.

!important is the true trump card. A declaration flagged important is lifted into a separate, higher-priority tier of the cascade:

.button { color: red !important; }
#cta    { color: blue; }  /* loses — the important red wins */

Even though #cta has a higher raw score, !important moves .button into a layer that outranks all normal declarations. When two important rules collide, the browser falls back to comparing their specificity normally.

Use !important as a last resort, not a habit. Every important you add is a rule that can only be beaten by another important, and specificity wars spiral fast. Reach for it to override third-party CSS you cannot edit, or for genuine utility overrides — not to patch your own cascade.

Why Your CSS Isn't Applying (The Real Reasons)

"Why is my CSS not applying?" is almost always one of these five, roughly in order of how often they bite:

  1. A more specific rule is overriding you. Open DevTools, inspect the element, and look at the Styles panel — the losing rule appears with a strikethrough. That is the browser telling you exactly what won.
  2. Two rules tie and yours comes first. Move your rule later or increase its specificity by one class.
  3. An !important upstream is beating your normal rule. Match it deliberately or, better, remove the upstream important.
  4. The selector doesn't match what you think — a typo, wrong class name, or an element that isn't actually a descendant.
  5. The property is inherited or invalid, so nothing is "winning" at all — the value you wrote was never parsed.

DevTools is the fastest specificity debugger you own. The struck-through declarations and the top-to-bottom order in the Styles panel are the cascade, rendered visually.

Modern Selectors That Change the Math

Three newer pseudo-classes give you direct control over specificity. All three are supported in every current browser (Chrome, Edge, Firefox, and Safari).

:where() — zero specificity on purpose

:where() always contributes (0, 0, 0), no matter what you put inside it. This is the modern tool for writing defaults that are trivially easy to override:

:where(.card) a {
  color: rebeccapurple;   /* specificity (0,0,1) — just the `a` counts */
}

Because the .card scope adds nothing, any later .card a { color: ... } rule at (0, 1, 1) wins effortlessly. Design-system and reset authors lean on :where() heavily so their base styles never fight application code.

:is() and :not() — take the highest argument

:is() and :not() adopt the specificity of their most specific argument:

:is(#main, .content) p { }   /* the #main pushes this to (1,0,1) */

Even if the element matched via .content, the whole selector scores as if the ID applied. :not(.hidden) scores like .hidden — one class. Knowing this stops :is() from silently inflating your specificity.

Pseudo-class Specificity contribution
:where(anything) always (0,0,0)
:is(#id, .class) the most specific argument — here (1,0,0)
:not(.class) the most specific argument — here (0,1,0)

Cascade Layers: Specificity's New Boss

@layer (supported in all current browsers) changes the order of operations. Rules in a later-declared layer beat rules in an earlier layer regardless of specificity:

@layer reset, base, components;

@layer base {
  #sidebar a { color: black; }  /* (1,0,0) — but lower layer */
}
@layer components {
  .link { color: blue; }        /* (0,1,0) — wins anyway */
}

Here .link wins even though #sidebar a has a far higher raw score, because components is declared after base. Layers let you retire ID-versus-class specificity battles entirely by deciding priority at the layer level instead. Unlayered styles beat all layered styles, which keeps the model predictable.

Keeping Specificity Low and Sane

The teams with the fewest cascade headaches follow a handful of habits:

Specificity is not really about memorizing a formula — it is about writing selectors flat enough that you rarely have to calculate anything. Once your rules mostly live in the same column, the cascade becomes boringly predictable, which is exactly what you want. From here, the natural next stop is how those winning rules actually size and space your elements — the CSS box model picks up right where specificity leaves off.

FAQ

How is CSS specificity calculated?

Count three things in your selector: IDs, then classes/attributes/pseudo-classes, then element types and pseudo-elements. Write them as (a,b,c) and compare left to right. A higher first column always wins, so a single ID (1,0,0) beats eleven classes (0,11,0). Columns never carry over into base-10 math; each is compared independently.

What is the order of specificity in CSS?

From weakest to strongest: the universal selector and combinators (zero), element types and pseudo-elements, then classes, attributes, and pseudo-classes, then IDs, then inline style attributes, and finally !important on top. Cascade layers and source order break ties, with later layers and later-written rules winning when raw scores are equal.

Does an ID always beat a class in CSS?

Yes, in normal specificity comparison. A single ID selector scores (1,0,0) and outranks any number of class selectors, because the browser compares the ID column before the class column. The only ways a class rule beats an ID rule are !important, an inline style, or a later cascade layer that reorders priority entirely.

Why is my CSS not applying?

Usually a more specific rule is overriding yours, or two rules tie and yours comes first in the source. Open DevTools and inspect the element: the losing declaration shows with a strikethrough. Other causes are an upstream !important, a selector that doesn't actually match, or an invalid property value that was never parsed.

How do I override a rule that uses !important?

The cleanest fix is to remove the upstream !important so normal specificity resumes. If you cannot edit that CSS, add your own declaration with !important and a specificity equal to or higher than the original, since important rules are compared against each other by ordinary specificity. Cascade layers can also help you avoid the fight altogether.

Does !important override inline styles?

Yes. An !important declaration in a stylesheet beats a normal inline style attribute. However, an inline style that itself uses !important outranks an important rule in a stylesheet. In practice the safest hierarchy to remember is: normal styles, then inline styles, then important stylesheet rules, then important inline styles at the very top.