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. The algorithm is explained on MDN's specificity reference.
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. Both selectors are detailed on MDN's `:is` reference.
/* 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. MDN's `@layer` documentation covers nested layers, anonymous layers, and the precedence rules in detail.
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:
- Inspect the element. Is the rule even hitting the element? Maybe the selector is wrong.
- Check for typos in the selector.
.nav-linkand.navlinkare different classes. - Look at what's winning in the Computed tab. If it's an inline style,
!importantis the only escape. If it's an ID selector from a library, refactoring is probably better. - Bump specificity minimally. If you need a rule to beat
.component .child, writing.component .child.modifieris 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.
FAQ
Why doesn't ten classes beat one ID?
The four columns in specificity (inline, ID, class, element) don't carry over — they're not a base-10 number. 0,0,10,0 is still less specific than 0,1,0,0. The reasoning is that IDs are intended to be unique identifiers (one per page), so the spec elevates them above class accumulation. In practice, this is one of the strongest arguments against using IDs for styling at all — they create cascade landmines that are hard to override without !important.
Should I use `@layer` in 2026?
If you're starting a new project or have spec-compliant browser support requirements (all evergreens since 2022), yes — @layer is the cleanest answer to specificity wars and works particularly well for design systems that ship base styles users can override. For existing codebases, you can adopt it incrementally because unlayered styles always beat layered styles, so introducing layers won't break existing overrides.
What's the right way to override a third-party widget's styles?
In order of preference: (1) Use the widget's CSS custom properties or theme API if it exposes one. (2) Write higher-specificity selectors that match what the widget already produces (.your-wrapper .their-button is usually enough). (3) Use @layer to put your overrides in a layer that beats the widget's layer. (4) Last resort, !important on the specific properties you need to win. Option 4 makes future maintenance harder, so save it for genuine dead ends.
Does `:not()` add specificity?
:not() itself contributes nothing, but its argument does — :not(.foo) adds the specificity of .foo (one class, 0,0,1,0). :not(#bar) adds an ID's worth (0,1,0,0). This is sometimes used as a workaround to bump specificity without adding meaningful selectors: .button:not(_) adds zero-impact specificity, but it's a code smell — if you need that, @layer or refactoring the cascade is usually a better answer.
When is `!important` actually correct to use?
Three cases: (1) Utility classes whose entire purpose is to win regardless of context (.sr-only, .hidden). (2) User-defined overrides like reader-mode styles in an extension or custom CSS injection. (3) Overriding inline styles you can't remove — inline styles have specificity (1,0,0,0) which only !important can beat (besides another inline !important). Outside these cases, !important is a sign of architectural drift.
How does the cascade decide between two `!important` declarations?
Specificity applies again — 0,1,0,0 !important beats 0,0,1,0 !important. If the specificity is equal, source order decides (later wins). If both are inline, the user agent processes them in the order they appear in the style attribute. The full priority order (highest to lowest) is: user !important → author !important → animations → author normal → user normal → user-agent.
Why does my pseudo-element style not apply?
Common causes: pseudo-elements need content: '' to render at all (except ::marker), so a ::before rule without content is invisible regardless of styling. Specificity is one element (0,0,0,1), so anything with a class wins. And ::before/::after only work on elements that can have children — they don't work on <img>, <input>, or other void elements. Use ::placeholder for input placeholder styling.
How do CSS Modules and scoped styles interact with specificity?
CSS Modules generate unique class names (e.g. Button_button__abc123) but the underlying selectors are still classes (0,0,1,0). Vue scoped styles and similar tools add an attribute selector ([data-v-abc]) to every rule, bumping specificity by 0,0,1,0. This makes scoped component styles harder to override from a parent, which is usually the desired behavior — but it's why deep selectors like :deep() exist as escape hatches.