The CSS Cascade and Specificity: Why Your Styles Don't Apply
When two CSS rules disagree, the browser follows a strict set of rules to pick a winner. Learn how the cascade decides, calculate specificity by hand, and stop reaching for !important.
On this page
You write a CSS rule. You reload. Nothing changes. You open DevTools and there it is, crossed out: another rule somewhere is winning. So you add a class, then an ID, then !important, and six months later nobody dares touch the stylesheet.
There's nothing random about which rule wins. When several declarations set the same property on the same element, the browser runs the cascade: a fixed sequence of tie-breakers. Learn the sequence and you can predict the winner every time, and write CSS that doesn't need !important at all. (If you're still getting comfortable with layout, CSS Flexbox vs Grid is a good first read.)
The cascade, in order#
For each property of each element, the browser collects every declaration that applies, then compares them in this order. The first step that finds a difference decides; later steps only break ties.
- Origin and importance. Your stylesheets beat the browser's default styles. A declaration marked
!importantbeats every normal one. - Inline styles. A
style="…"attribute beats any selector in a stylesheet. - Cascade layers. If you use
@layer, styles in later layers beat earlier ones, whatever their selectors say. - Specificity. The more specific selector wins.
- Order of appearance. If everything else is equal, the declaration that comes last wins.
Most day-to-day surprises happen at steps 4 and 5, so let's start there.
Calculating specificity#
Specificity is a score made of three numbers, usually written (A, B, C):
- A: the number of ID selectors (
#header) - B: the number of class selectors (
.card), attribute selectors ([type="email"]) and pseudo-classes (:hover,:focus,:nth-child()) - C: the number of type selectors (
p,button) and pseudo-elements (::before)
The universal selector * and combinators (, >, +, ~) count for nothing.
| Selector | Specificity |
|---|---|
p |
(0, 0, 1) |
.card p |
(0, 1, 1) |
.card .title |
(0, 2, 0) |
a:hover |
(0, 1, 1) |
input[type="email"] |
(0, 1, 1) |
ul li::before |
(0, 0, 3) |
#sidebar a |
(1, 0, 1) |
#nav .item a:hover |
(1, 2, 1) |
* |
(0, 0, 0) |
To compare two selectors, compare A first. Only if A is equal do you look at B, and only if B is equal do you look at C. There's no carrying over between columns: one ID (1, 0, 0) beats a selector with twenty classes (0, 20, 0).
Here's a classic surprise:
#main .button { background: gray; } /* (1, 1, 0) */
.button.primary { background: blue; } /* (0, 2, 0) */<div id="main">
<button class="button primary">Save</button> <!-- gray! -->
</div>The primary button is gray, even though .button.primary looks more specific to a human and comes later. The ID wins at column A, and step 5 is never reached.
Let DevTools do the maths
In Chromium-based browsers, hover over a selector in the Styles pane to see its specificity. The Computed tab shows the final value of each property and, when you expand it, every rule that tried to set it, winner first.
:is(), :not(), :has() and :where()#
These pseudo-classes take a list of selectors, and they count differently:
:is(),:not()and:has()take the specificity of their most specific argument.:is(#nav, .menu) ais (1, 0, 1), even when it matches through.menu.:where()always has zero specificity, whatever is inside it.
That makes :where() perfect for defaults that should be easy to override:
/* (0, 0, 1): any later class beats it */
:where(.prose) a { color: royalblue; }
/* (0, 1, 1): would beat most component styles, which is usually not what you want */
.prose a { color: royalblue; }Inheritance is not the cascade#
Some properties, mostly text-related ones like color, font-family and line-height, are inherited: if nothing sets them on an element, it takes its parent's value. Others, like margin, border and background, are not.
The trap: an inherited value always loses to any rule that targets the element directly, however weak that rule's selector is.
#article { color: darkred; } /* (1, 0, 0), but applies to the div */
p { color: black; } /* (0, 0, 1), applies to the p itself */<div id="article">
<p>This text is black, not dark red.</p>
</div>Specificity only compares rules that match the same element. The #article rule matches the div; the paragraph only inherits from it, and inheritance only kicks in when no rule sets color on the paragraph itself.
Four keywords let you control this explicitly on any property:
| Keyword | Meaning |
|---|---|
inherit |
Take the parent's value |
initial |
Use the property's default value from the CSS spec |
unset |
inherit for inherited properties, initial for the others |
revert |
Go back to what the browser's own stylesheet would set |
!important and inline styles#
!important jumps the queue at step 1, before specificity is even looked at. That's exactly why it causes trouble: the only way to override an !important declaration is with another !important declaration that wins on the later steps, and so the arms race begins.
Inline style attributes (step 2) beat any selector, but lose to !important in a stylesheet.
Keep !important for utilities
A reasonable use is a small utility class that must always win, like .hidden { display: none !important; }. Using it to fix a specificity problem only hides the problem until the next one.
Cascade layers: specificity under control#
Cascade layers (@layer, supported in all major browsers since 2022) add a step above specificity. You declare the order of your layers once, and from then on a later layer beats an earlier one, whatever the selectors:
@layer reset, vendor, components, utilities;
@layer vendor {
#app .btn { background: gray; } /* (1, 1, 0) */
}
@layer components {
.btn { background: royalblue; } /* (0, 1, 0), and it wins */
}components comes after vendor in the @layer statement, so its .btn wins despite much lower specificity. This is ideal for third-party CSS: wrap it in an early layer (@import url("library.css") layer(vendor);) and your own styles override it without specificity tricks.
Two details worth knowing: styles that aren't in any layer beat all layered styles, and for !important declarations the layer order is reversed, so an early layer's !important beats a later one's.
Habits that prevent specificity wars#
- Style with classes, not IDs. Keep IDs for anchors and JavaScript.
- Keep selectors short and flat.
.card-titleinstead of.sidebar .card > h2.title. Flat selectors all have similar specificity, so source order and layers decide, and those are easy to reason about. - Wrap defaults and resets in
:where()so they're trivial to override. - Use
@layerto put third-party and base styles below your components. - When a rule doesn't apply, check DevTools first: is it crossed out (lost the cascade), missing (selector doesn't match), or shown with a warning (invalid value)?
Recap#
- The cascade picks a winner in a fixed order: origin and
!important, inline styles, layers, specificity, then order of appearance. - Specificity is (IDs, classes/attributes/pseudo-classes, elements/pseudo-elements), compared column by column, with no carrying over.
:is(),:not()and:has()take their strongest argument's specificity;:where()adds zero.- Inherited values lose to any rule that targets the element directly.
- Prefer flat class selectors,
:where()and@layerover IDs and!important.