What Is The CSS Cascade
The CSS cascade is the rule system browsers use to decide which CSS declaration wins when multiple declarations try to style the same thing.
Beginners often think “the cascade” means “whatever is written last”. That’s only one piece. In reality, the browser walks through a few steps and picks a winner like it’s judging a weird talent show: relevance, then origin and importance, then specificity, then finally source order.
.title {
color: rebeccapurple;
}
.title {
color: seagreen;
}
.title {
color: tomato;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
max-width: 960px;
font-family: system-ui, Arial, sans-serif;
}
.title {
padding: 14px 16px;
border: 3px solid #111;
background: #fff;
font-size: 28px;
line-height: 1.2;
}
.note {
margin-top: 10px;
font-size: 14px;
opacity: 0.85;
}
Which color wins?Click the snippets: you are simulating “multiple rules competing”. The cascade picks a winner.
In that playground you’re manually toggling which rules exist, but on real sites, all those rules can coexist, and the browser must pick one winner.
The Cascade Decision Tree
When multiple declarations target the same element and property, the browser roughly follows this order:
- Relevance: Does the selector match? Is the declaration valid? Is its media query active?
- Origin and importance: Where did the rule come from, and is it
!important? - Specificity: How “targeted” is the selector?
- Source order: If everything above ties, the later rule wins.
We’ll go through each with interactive examples, plus a few “why is CSS doing this to me” gotchas.
Step 1: Relevance
A declaration can only compete if it’s actually in the running:
- The selector must match the element.
- The declaration must be valid (invalid properties or invalid values get ignored).
- If the rule is inside a media query, that media query must currently match.
Relevance: Selector Matching
If a selector doesn’t match, it’s not “losing” the cascade. It’s simply not participating.
.card {
border-color: hotpink;
}
.card.special {
border-color: dodgerblue;
}
.special {
border-color: goldenrod;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
max-width: 960px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 14px;
}
.card {
padding: 14px 16px;
border: 5px solid #111;
background: #fff;
border-radius: 14px;
}
.card h4 {
margin: 0 0 8px;
font-size: 18px;
}
.card p {
margin: 0;
opacity: 0.9;
}
Card A
I am
.cardCard B
I am
.card.special
Notice how .card.special can only affect the second card because only the second card matches.
Relevance: Invalid Declarations Get Ignored
CSS is forgiving, but not that forgiving. If a value is invalid, the browser discards that declaration like “nope, not today”.
.badge {
color: blurple;
}
.badge {
color: slateblue;
}
.badge {
color: #0b6;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
max-width: 960px;
font-family: system-ui, Arial, sans-serif;
}
.badge {
display: inline-block;
padding: 10px 14px;
border: 3px solid #111;
border-radius: 999px;
background: #fff;
font-weight: 700;
letter-spacing: 0.02em;
}
If the value is invalid, it is ignored
blurple isn’t a valid color keyword, so that declaration is ignored. The element keeps whatever other rule wins, or falls back to default styling.
Step 2: Origin And Importance
After relevance, the browser considers where the CSS came from and whether it’s marked as !important.
In normal web dev, you mostly deal with author styles (your CSS). But there are also:
- User-agent styles: the browser default stylesheet (links are blue, headings are bold, etc).
- User styles: accessibility or custom user styles a person may apply.
And then there’s !important, which boosts a declaration within its origin category.
Importance: !important Beats Not Important
Within the same origin (your styles), a declaration marked !important beats a normal declaration, even if the normal declaration is more specific or later.
.notice {
border-color: crimson !important;
}
#app .notice {
border-color: seagreen;
}
*,
::before,
::after {
box-sizing: border-box;
}
#app {
padding: 18px;
max-width: 960px;
font-family: system-ui, Arial, sans-serif;
}
.notice {
padding: 14px 16px;
border: 5px solid;
border-radius: 14px;
font-size: 18px;
line-height: 1.3;
}
Which border wins? (Hint:!importantis loud.)
Even though #app .notice is more specific, it loses to border-color: crimson !important; because importance is evaluated before specificity.
If you want to override an !important declaration, you typically need your own !important declaration (and then specificity and source order matter again within the “important” context).
Step 3: Specificity
Specificity answers: “How precisely does this selector target the element?”
- Inline styles (style attribute) are very strong in the normal cascade.
- ID selectors beat classes.
- Class selectors beat element selectors.
Specificity only matters when declarations are competing at the same origin and importance level.
Specificity Basics With A Simple Target
p {
color: steelblue;
}
.tip {
color: rebeccapurple;
}
#box .tip {
color: tomato;
}
*,
::before,
::after {
box-sizing: border-box;
}
#box {
padding: 18px;
max-width: 960px;
font-family: system-ui, Arial, sans-serif;
}
.panel {
padding: 14px 16px;
border: 3px solid #111;
background: #fff;
border-radius: 14px;
}
p {
margin: 0;
font-size: 18px;
}
I am a
p.tipinside#box
Here’s the mental shortcut:
p is broad, .tip is more targeted, and #box .tip is the most targeted.
Specificity Trap: “More Classes Beat An ID”, Nope
People sometimes try to “out-class” an ID. But one ID selector is stronger than any number of class selectors.
#alert {
background: #ffe8e8;
}
.card.warning.urgent.loud {
background: #fff7d6;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
max-width: 960px;
font-family: system-ui, Arial, sans-serif;
}
.card {
padding: 14px 16px;
border: 3px solid #111;
background: #fff;
border-radius: 14px;
font-size: 18px;
}
I have anidand a bunch of classes.
#alert beats .card.warning.urgent.loud. If you want to beat an ID selector without !important, you generally need another ID selector (or remove the ID-based rule).
Learn more about specificity in the CSS Specificity Interactive Tutorial.
Step 4: Source Order
If two declarations are equally relevant, same origin, same importance, and have the same specificity, the one written later wins.
.button {
transform: translateY(0);
}
.button {
transform: translateY(-6px);
}
.button {
transform: translateY(6px);
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
max-width: 960px;
font-family: system-ui, Arial, sans-serif;
}
.button {
display: inline-block;
padding: 12px 16px;
border: 3px solid #111;
background: #fff;
border-radius: 12px;
font-weight: 700;
cursor: pointer;
user-select: none;
}
In real projects, source order includes things like:
- The order of your
<link>stylesheets. - The order of rules inside the same stylesheet.
- The order of
@import(imports are treated as if their contents appear where the import is placed).
Inheritance Vs Cascade
Inheritance is not the cascade. They work together, but they’re different systems.
- Inheritance: some properties naturally pass from parent to child (like
color,font-family). - Non-inherited properties: many do not inherit (like
border,padding). - The cascade: decides the winning value on each element for each property.
.container {
color: rebeccapurple;
}
.child {
color: seagreen;
}
.child {
color: inherit;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
max-width: 960px;
font-family: system-ui, Arial, sans-serif;
}
.container {
padding: 14px 16px;
border: 3px solid #111;
background: #fff;
border-radius: 14px;
}
.child {
margin-top: 12px;
padding: 12px 14px;
border: 3px dashed #111;
border-radius: 12px;
}
.small {
margin-top: 10px;
font-size: 14px;
opacity: 0.85;
}
Parent text (container)Child text (child)Some properties inherit by default. Others do not. You can also force inheritance withinherit.
When you set .container { color: ... }, the child gets that color unless it has its own winning color value.
Cascade Layers
Cascade layers let you group rules into “priority buckets” that sit inside the cascade. This is extremely useful when mixing:
- Reset styles
- Base typography
- Components
- Utilities
- Third-party CSS
In plain language: layers help you avoid specificity wars by deciding, up front, which group should win.
Layers: A Simple Mental Model
If two rules compete, the browser first checks layer order before specificity (within normal author styles). Later layers win over earlier layers.
@layer base {
.tag {
background: #f2f2f2;
}
}
@layer utilities {
.tag {
background: #ffe08a;
}
}
@layer base {
.tag {
background: #f2f2f2;
}
}
@layer utilities {
.tag.is-hot {
background: #ffb3b3;
}
}
@layer utilities {
.tag {
background: #ffe08a;
}
}
@layer base {
.tag {
background: #f2f2f2;
}
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
max-width: 960px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 14px;
}
.row {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.tag {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 3px solid #111;
border-radius: 999px;
font-weight: 700;
}
.hint {
font-size: 14px;
opacity: 0.85;
}
Normal HotLayers are ordered by their first appearance. Later layers usually win, but order matters.
In snippet 3, @layer utilities is declared before @layer base, which changes who gets the final say.
Layer order is part of the cascade, so it can flip “expected” outcomes if you’re not watching it.
Shorthands And Longhands
The cascade chooses winners per property. But shorthands complicate things because one shorthand can set many longhands.
Example: border affects border-width, border-style, and border-color.
.box {
border: 6px solid #111;
}
.box {
border: 6px solid #111;
border-color: tomato;
}
.box {
border-color: tomato;
border: 6px solid #111;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
max-width: 960px;
font-family: system-ui, Arial, sans-serif;
}
.box {
padding: 18px;
background: #fff;
border-radius: 14px;
font-size: 18px;
}
Shorthand vs longhand: order matters when specificity ties.
In snippet 2, the longhand border-color comes after border, so it wins (same specificity and origin).
In snippet 3, the shorthand comes last and resets the color back to #111.
This is a classic “why did my border color revert” bug.
Custom Properties And The Cascade
CSS variables (custom properties) follow the cascade too. The browser determines the winning value for --my-var on an element, and then uses it when computing other properties.
:host {
--accent: rebeccapurple;
}
.card {
border-color: var(--accent);
}
:host {
--accent: rebeccapurple;
}
.theme-ocean {
--accent: dodgerblue;
}
.card {
border-color: var(--accent);
}
:host {
--accent: rebeccapurple;
}
.card {
--accent: seagreen;
border-color: var(--accent);
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
max-width: 960px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 14px;
}
.card {
padding: 14px 16px;
border: 6px solid;
background: #fff;
border-radius: 14px;
font-size: 18px;
}
.hint {
font-size: 14px;
opacity: 0.85;
}
Card ACard B inside.theme-oceanCustom properties cascade: closer definitions override farther ones.
Here we are using :host because the playgrounds use a shadow DOM. In a real project, you would probably use :root.
Notice how defining --accent on a wrapper affects everything inside it, and defining it directly on .card is even more local.
A Practical Debugging Checklist
When “CSS is not working”, it’s usually the cascade doing exactly what it was built to do. Here’s a quick checklist you can run through:
- Is the rule relevant? Does the selector match the element you think it matches?
- Is the declaration valid? Any typos in the property or value?
- Is a media query blocking it? Resize the viewport or check the query conditions.
- Is there an
!importantsomewhere? If yes, it changes the game. - Is your selector losing on specificity? Compare the winning selector vs yours.
- Is your stylesheet loaded earlier? Source order matters across files too.
- Are shorthands resetting your longhands? Look for
border,background,font, etc. - Are layers involved? A lower layer can lose even with higher specificity.
DevTools Move: Find The Winning Rule
In browser DevTools (Elements panel), click the element, then look at the “Styles” pane:
- Crossed-out declarations lost the cascade.
- The non-crossed-out value is the winner.
- You can usually see the file and line number where it came from (source order clue).
If you get good at reading crossed-out rules, you basically become a CSS wizard.
Mini Recap: The Cascade In One Sentence
The cascade picks the winning CSS declaration by checking relevance, then origin and importance, then specificity, and finally source order.
Your New Mental Model
When you see two rules fighting, don’t guess. Ask:
- Are both rules actually applied and valid?
- Is one
!importantor from a different “priority context” (like layers)? - Which selector is more specific?
- If tied, which one is later?
Do that consistently and the cascade stops feeling random.
