CSS Specificity: The Complete Guide to the Cascade and Inheritance

CSS Specificity: The Complete Guide to the Cascade and Inheritance

At some point every CSS developer has stared at a rule that should be working and typed !important in frustration. Usually it works. Sometimes it makes things worse. Understanding specificity means you stop fighting the cascade and start working with it — debugging gets dramatically faster.

What the Cascade Actually Does

The cascade is the algorithm browsers use to decide which CSS rule wins when multiple rules target the same element and property. It considers three things in order: origin and importance, specificity, and source order.

Most of the time you're working within a single stylesheet and the same importance level, so specificity is what matters. Specificity is a score the browser assigns to each selector. Higher score wins.

The Four-Column Scoring System

Specificity is represented as four numbers: inline : ID : class : element.

Think of them as columns in a number with a very large base. A score of 0,1,0,0 always beats 0,0,10,0 — ten class selectors don't add up to beat a single ID, because the columns don't carry over.

Here's how each selector type contributes:

Selector type Column Example
Inline style 1,0,0,0 style="color: red"
ID 0,1,0,0 #header
Class, attribute, pseudo-class 0,0,1,0 .nav, [type="text"], :hover
Element, pseudo-element 0,0,0,1 div, p, ::before
Universal selector, combinators 0,0,0,0 *, >, +, ~

Let's calculate a few real selectors:

/* 0,0,0,1 — one element */
p { }

/* 0,0,1,1 — one class + one element */
p.intro { }

/* 0,1,1,1 — one ID + one class + one element */
#sidebar .nav a { }

/* 0,0,2,1 — two pseudo-classes + one element */
a:hover:focus { }

/* 0,0,1,2 — one attribute + two elements */
input[type="text"] + label { }

When two rules target the same property on the same element, the higher specificity score wins — regardless of which comes first in the stylesheet.

Source Order as the Tiebreaker

When specificity is equal, the rule that appears later in the stylesheet wins. This is why the order of your stylesheets matters, and why reset/base styles should come before component styles:

/* Both 0,0,1,0 — source order decides */
.button { background: blue; }
.cta    { background: green; } /* wins if element has both classes */

This also explains why CSS methodologies like BEM stick to a single class for each rule — when every selector has the same specificity weight, source order becomes predictable and you can reason about overrides without specificity math.

Inheritance Is Not Specificity

A rule that directly targets an element always beats one that's inherited from a parent, regardless of specificity. Inherited values have no specificity at all — they're just defaults that apply when nothing more specific exists.

body { color: red; }     /* inherited by p */
p    { color: blue; }    /* directly targets p — wins */

Properties like color, font-family, and line-height inherit by default. Layout properties like margin, padding, and display don't. You can force inheritance with inherit or reset to default with initial and unset.

`!important`: When It's Actually the Right Tool

!important overrides specificity entirely. Any !important declaration beats any normal declaration, and among !important declarations, specificity applies again.

The instinct to reach for it is usually a sign that the architecture has gotten tangled, and adding !important patches the symptom without fixing the structure. But there are legitimate uses:

/* Utility classes that must always apply */
.sr-only {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  overflow: hidden !important;
  clip: rect(0, 0, 0, 0) !important;
}

/* Overriding third-party widget styles you can't touch */
.widget-container .vendor-button {
  background: var(--brand-color) !important;
}

The two valid cases: utility classes that must be immune to override by accident, and dealing with third-party CSS you can't modify. Outside those cases, if you need !important to make something work, the selector architecture is the real problem.

`:is()` and `:where()` and How They Affect Specificity

:is() and :where() both let you write compound selectors more concisely, but they handle specificity differently — and this trips people up.

:is() takes the specificity of its most specific argument:

/* 0,1,0,0 — because #main is an ID */
:is(#main, .content, p) a { }

:where() always has zero specificity:

/* 0,0,0,1 — only the `a` contributes */
:where(#main, .content, p) a { }

This makes :where() excellent for base styles and resets — it applies sensible defaults that any normal class selector can override without needing to raise the specificity counter.

/* Base link styles — zero specificity, easy to override anywhere */
:where(a) {
  color: var(--link-color);
  text-decoration: underline;
}

CSS Layers: The Modern Fix for Specificity Wars

@layer is the cleanest solution to long-term specificity management. Layers let you explicitly order groups of styles, and lower layers always lose to higher layers — regardless of specificity within those layers:

@layer reset, base, components, utilities;

@layer reset {
  /* Browser reset — always lowest priority */
  * { box-sizing: border-box; margin: 0; }
}

@layer base {
  /* Typography and defaults */
  body { font-family: system-ui, sans-serif; }
}

@layer components {
  /* Component styles */
  .button { padding: 0.5rem 1rem; background: blue; }
}

@layer utilities {
  /* Utilities — highest priority, always wins */
  .hidden { display: none; }
}

With layers, a low-specificity utility class in the utilities layer beats a high-specificity component selector in the components layer. You define the precedence by the layer order, not by selector complexity.

Unlayered styles (anything not inside a @layer) beat all layered styles, so you can gradually adopt layers in existing codebases without breaking everything.

Practical Debugging Tips

When a style isn't applying and you can't figure out why, open the DevTools Computed tab. It shows which rule is actually winning for every property, crosses out the losers, and shows the winning selector clearly.

A few fast checks before reaching for !important:

  1. Inspect the element. Is the rule even hitting the element? Maybe the selector is wrong.
  2. Check for typos in the selector. .nav-link and .navlink are different classes.
  3. Look at what's winning in the Computed tab. If it's an inline style, !important is the only escape. If it's an ID selector from a library, refactoring is probably better.
  4. Bump specificity minimally. If you need a rule to beat .component .child, writing .component .child.modifier is cleaner than adding !important.

The CSS Minifier is useful once you've got specificity sorted — it strips comments, whitespace, and redundant declarations so the rules that matter are the only ones the browser has to process.

For the full context on how CSS interacts with JavaScript and the rendering pipeline, CSS Custom Properties Explained is a natural next read — custom properties live in the cascade too, and understanding specificity makes their scoping behavior click. And for a look at how different layout systems interact with specificity in practice, Flexbox vs Grid covers the layout layer on top of these cascade fundamentals.