CSS cascade
Browser as a runtime · part 1 of 6. Next: Stacking contexts.
The browser has more than one machine. This one picks which declaration wins for a property — the color, the font, the display. It does not pick who paints on top. That is part 2. Specificity is not the whole cascade.
Try this before you read anything else
Section titled “Try this before you read anything else”The paragraph has style="color: teal" on the element itself. Your stylesheet says p { color: navy }. Inline should win forever, right?
Play it. Then click the button.
You barely changed anything. The inline teal is still on the tag. The navy rule is still in the stylesheet. You added a class: color: crimson !important. And yet the class won.
Sit with that for a second. Why would a class beat an inline style?
What the cascade actually is
Section titled “What the cascade actually is”The cascade is not a shouting contest. It is a stack of in-trays on a desk. The browser picks the top tray that has a note for this property. Only then does it look at handwriting.
Analogy: office in-trays, not a yelling match.
- Each tray is an origin plus importance — browser defaults, your CSS, your CSS stamped urgent.
- Inside a tray, bigger handwriting wins (
#idbeats.classbeatsp). Inlinestyle=""is the biggest handwriting in the normal author tray. - Same handwriting, the later note in the file wins.
- A class cannot climb into a higher tray by shouting.
!importantis a different tray, sitting on top of all the normal author notes.
When you stamp !important, you are not making the selector more specific. You are moving the note to the urgent tray.
The picture treats inline as its own step because no selector beats style="". Under the hood that is still author CSS — just unbeatable handwriting in the normal tray. !important is the step that is actually a new comparison.
Back to the demo — why crimson won
Section titled “Back to the demo — why crimson won”The paragraph started with two notes in the same tray: p { color: navy } and style="color: teal". Same origin. Same importance. Inline has bigger handwriting, so teal wins. You saw that first.
Then we added .loud { color: crimson !important }. Any author !important changes trays. The urgent tray sits above the whole normal author pile.
Now the comparison is:
- Rank the trays. Urgent author beats normal author. Crimson is urgent. Teal is still normal. Urgent wins.
- Inside the normal tray, teal still beats navy — but that fight never gets a meeting with crimson.
That is why adding a more specific selector is the wrong next move. Inspect which tray won. If the winner is !important, shouting louder in the normal tray does nothing. If the winner is the style attribute, remove the inline (or stamp yours urgent too). Inflating the class list does nothing against a higher tray.
Same fight, other moves
Section titled “Same fight, other moves”People reach for a later rule or a heavier selector. Those are still the same tray as the inline style. The second demo lets you try each move against that same teal style="".
Specificity
Section titled “Specificity”What it is: a score for how targeted a selector is. Inline style is the top score a selector cannot reach. Then IDs, then classes / attributes / pseudo-classes, then elements.
Why it matters here: specificity only runs after origin and importance. Two notes in the same tray: bigger handwriting wins. A #id cannot beat style="". A #id cannot beat a class with !important.
#hero { color: navy; } /* loses to style="color: teal" */Source order
Section titled “Source order”What it is: when two rules have the same origin, importance, and specificity, the one that appears last in the document wins.
Why it matters here: “I’ll just put it later in the file” only works inside a tie. Inline is not a tie. An ID is not a tie with a class.
.a { color: navy; }.a { color: teal; } /* same tray, same handwriting — teal wins */!important
Section titled “!important”What it is: a flag on a declaration, not on a selector. color: crimson !important stamps that property urgent.
Why it matters here: it changes trays. Author !important sits above all normal author CSS, including inline styles. It is not extra specificity. A later !important beats an earlier one. An inline !important beats a stylesheet !important — same urgent tray, bigger handwriting.
.loud { color: crimson !important; } /* class, but urgent tray */Inheritance
Section titled “Inheritance”What it is: if this element has no cascaded value for a property, it may take the parent’s computed value (color, font-family). Some properties do not inherit (margin, display).
Why it matters here: people say “this color cascaded down.” That was inheritance. The cascade had nothing to compare. There was no note in any tray.
article { color: navy; }/* p has no color rule — it inherits navy */How this bug shows up in real UI
Section titled “How this bug shows up in real UI”- A React
style={{}}or a leftoverstyle=from a library. You add classes. Nothing happens. Then!important. It “works.” - Two stylesheets: yours and a design system. Import order is the source-order fight. An ID in the design system still beats your later class.
- A utility with
!important(old Bootstrap, some CSS-in-JS) turns every override into a tray fight.
Fix the tray, not the shouting. Remove the inline, or match importance if you truly have to override a library stamp. Heavier selectors do nothing against a higher tray.
Inspect the element. Open the property. The winner is listed first. Read why it won before you write another selector.
The cascade picked the color. Stacking decides who is painted on top. Those are different machines — next in this series.