The Cascade is one of those fundamental CSS concepts that’s easily taken for granted. But the longer you work in large projects, the clearer it becomes: It’s not just a mechanism for resolving style conflicts—it’s the foundation of any robust CSS architecture.
In my projects, I constantly see how much effort teams could save if the Cascade were understood not just theoretically, but truly practically. That’s why this Advent calendar intentionally starts right here.
What the Cascade Actually Decides
Put simply, the job of the Cascade is this: When multiple rules apply to the same element, it decides which one wins. It follows four principles:
- Origin – Where does the rule come from?
- Importance – Is it marked with
!important? - Specificity – How precise is the rule formulated?
- Order – Which rule comes later?
It sounds simple, but in real codebases, it unfolds a surprising amount of complexity. Time to unravel it.
1. Origins: Where Does the Rule Come From?
The browser distinguishes between different sources:
- User Agent Styles (The browser’s default styles)
- Author Styles (Your CSS)
- User Styles (Styles modified by the user, e.g., for accessibility)
In reality, this mostly plays a role when default styles get in the way of your own styles. For example, with buttons, form fields, or lists.
A classic example:
button {
background: none;
border: none;
}
Why does the button still look like that? Because browsers often have more complex internal rules, sometimes even with higher specificity.
If you don’t know this, you quickly start writing unnecessary overrides.
2. Importance: The Last Resort – Or Is It?
!important should be used sparingly or, better yet, avoided entirely. We know that. But in reality, it ends up in the code more often than we’d like.
It gets interesting when important rules compete against each other:
!importantin the Author Stylesheet wins against Browser Styles.- But a User Stylesheet with
!importantbeats everything.
This is especially relevant for accessibility—a topic easily forgotten when writing CSS.
A real-world project example:
.text {
color: var(--text-color) !important;
}
Sounds harmless, but it led to users in High Contrast Mode suddenly seeing nothing. Here, a better understanding of Importance—and especially the alternatives—helps.
3. Specificity: The Classic Conflict Source
Specificity is the criterion that causes the most misunderstandings. It is often explained with values like 1000, 100, 10, and 1. This works as a mnemonic, but is technically not quite correct.
Browsers actually calculate specificity as a tuple of three values (A, B, C):
- A (IDs): Number of IDs in the selector
- B (Classes, Attributes, Pseudo-Classes): Number of these elements
- C (Elements, Pseudo-Elements): Number of these elements
Comparison happens column by column: A single ID in column A beats infinite classes in column B. The often-used model “10 classes make an ID” is therefore wrong.
An example from a real enterprise project:
/* Component Style */
.card .title {
font-weight: 500;
}
/* Global Override */
.title {
font-weight: 700;
}
At first glance, you might think: Global comes later, so it wins.
But: .card .title has a higher specificity—(0, 2, 0) instead of (0, 1, 0).
The result was a bug that cost hours to fix.
Getting Specificity Under Control
An often underestimated tool is :where():
:where(.card .title) {
font-weight: 500;
}
:where() sets the specificity to 0, no matter how complex the selector is.
This makes components less fragile and makes overrides massively easier.
4. Order: The Subtle but Important Rule
If all other factors are equal, the later rule wins.
In modular systems—often driven by tools like Vite, Webpack, or Astro—this quickly leads to unexpected behavior because styles from different files don’t always end up in the order you wrote them.
An example:
/* base.css */
.button {
padding: 0.75rem;
}
/* components.css */
.button {
padding: 1rem;
}
Theoretically, components.css should win.
But if a bundler swaps the order, the result is different.
Here, CSS Layers (Day 14) provide significantly more stability.
CSS Layers: Structure for the Cascade
With @layer, CSS has gained a powerful tool for consciously controlling the Cascade. Layers allow you to explicitly define priorities—independent of specificity or source order.
How Layers Work
Layers add an additional level to the Cascade. The priority between layers is determined by their declaration order, not by specificity:
/* Define layer order */
@layer reset, base, components, utilities;
@layer reset {
* {
margin: 0;
padding: 0;
}
}
@layer components {
.button {
padding: 0.75rem;
background: var(--primary);
}
}
@layer utilities {
.p-0 {
padding: 0 !important;
}
}
The crucial point: A selector with low specificity in a later layer beats a highly specific selector in an earlier layer.
@layer base {
#header .button {
background: blue; /* Specificity (1, 1, 0) */
}
}
@layer components {
.button {
background: red; /* Specificity (0, 1, 0) */
}
}
Here, .button wins with red—even though #header .button has significantly higher specificity. The layer is decisive, not the specificity.
Why This Helps in Practice
In large projects, specificity battles often lead to overly complex selectors or !important escalations. Layers help avoid this:
@layer designsystem, app-overrides;
@layer designsystem {
/* Design system with arbitrary specificity */
.card .title {
font-size: 1.25rem;
}
}
@layer app-overrides {
/* App-specific adjustments automatically win */
.title {
font-size: 1.5rem;
}
}
No more complex selectors needed. The architecture is expressed through the layer structure, not through specificity tricks.
The Position of Layers in the Cascade
Layers fit into the Cascade between “Origin” and “Specificity”:
- Origin & Importance
- Context (e.g., Encapsulation like Shadow DOM)
- Element-attached Styles (Inline styles)
- Layers ← here!
- Specificity
- Order
This means: Layers carry more weight than specificity, but less than inline styles or !important.
Why the Cascade is Crucial for Good Architecture
If you understand the Cascade, you fight less against CSS and work more with the browser. This leads to:
- fewer unnecessary overrides
- less
!important - a clearer structure
- more stable components
- better maintainability
But above all, it leads to CSS that offers predictability. That is a factor that decides between success and chaos in large codebases.
A Small Practical Example from Daily Life
A common conflict in projects with a design system:
/* Design System */
.btn {
background: var(--primary-500);
}
/* App-specific */
.header .btn {
background: var(--brand-blue);
}
Then a developer gets the idea to set an inline style:
<button class="btn" style="background: red"></button>
Inline styles have higher priority than any selector in the stylesheet. Sometimes they are simplified as a ‘1’ in an imaginary fourth column (e.g. (1, 0, 0, 0)). The component-based override of .header .btn (specificity (0, 2, 0)) is rendered useless.
Cases like this are commonplace in large projects. And almost always, the cause is a lack of understanding of the Cascade.
Conclusion
The Cascade is not a CSS side show. It is the central decision-making model that determines how styles are ultimately applied. If you don’t understand it, you’re optimizing symptoms. If you master it, you write CSS that is sustainable and significantly more stress-free.
That is exactly why this Advent calendar starts with it. Everything else builds on this.
Comments