Door 1 – Understanding the Cascade | CSS Adventskalender
Skip to content

Door 1 – Understanding the Cascade

Published: at 06:00 AM

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:

  1. Origin – Where does the rule come from?
  2. Importance – Is it marked with !important?
  3. Specificity – How precise is the rule formulated?
  4. 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:

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:

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):

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”:

  1. Origin & Importance
  2. Context (e.g., Encapsulation like Shadow DOM)
  3. Element-attached Styles (Inline styles)
  4. Layers ← here!
  5. Specificity
  6. 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:

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.


☕ Buy me a coffee

If you enjoy my articles and they help you in your work, I would appreciate a "coffee" and a few kind words from you.

Buy me a coffee

Previous Post
Door 2 – Mastering Specificity

Comments