Door 14 – CSS Cascade Layers | CSS Adventskalender
Skip to content

Door 14 – CSS Cascade Layers

Published: at 07:00 AM

With @layer, CSS has received one of the most significant architectural features in years. While the classic Cascade and specificity remain valid, @layer allows an additional level of control. We can define which styles should fundamentally take precedence – regardless of specificity or file order.

For large codebases, design systems, or projects with many involved teams, this is a game changer.

Why Do We Need Cascade Layers?

The traditional CSS Cascade has a central weakness. It is based on specificity and file order and both are difficult to control in modern projects. In a project with multiple teams, various data sources, and a complex build system, it is nearly impossible to predict which rule will win in the end.

The problems often begin with the structure itself. Styles come from different files, global resets, theme definitions, component styles, utility classes. Dynamic bundlers like Webpack, Vite, or Astro mix these files in an order that can change depending on the build configuration. Inline styles in HTML take precedence over external stylesheets, but not over !important rules. Component styles in React, Vue, or Svelte are injected at runtime, often in unpredictable order. Framework overwrites override design system styles and utility classes should actually be dominant, but lose against highly specific selectors.

The result is a constant battle. Developers increase specificity to override existing rules. This leads to selectors like .page .section .card .button, which are difficult to maintain and make the entire Cascade fragile. When that’s not enough, !important comes into play, which only makes the situation worse because it effectively overrides the Cascade. Workarounds emerge through additional classes, nested selectors, duplicate rules. Complexity grows exponentially, and maintainability decreases.

With @layer, this battle can be solved at the root. Instead of relying on specificity and file order, you explicitly define which styles should take precedence. This makes the Cascade predictable, drastically reduces the need for !important, and creates a clear, documented architecture.

How Do Layers Work?

A layer defines a position in the Cascade, completely independent of specificity or file order. This is the crucial difference to traditional CSS. While normally a selector with higher specificity wins (e.g., .header .button beats .button), with layers the layer order decides, not specificity.

This means concretely: A selector in a later layer always wins against a selector in an earlier layer, even if the specificity is lower. A simple .button in a utilities layer beats a highly specific #header .navigation .button.primary in a components layer, as long as utilities was declared after components.

This decoupling from specificity is what makes layers so powerful. You no longer need to work with nested selectors or !important to override styles. Instead, you organize styles into logical layers and define once which layer takes precedence. The rest follows automatically.

Example:

@layer reset, base, components, utilities;

With this, we define a clear sequence:

  1. reset – the lowest priority
  2. base – basic element styles
  3. components – component-specific styles
  4. utilities – the highest priority

Even if a component style stands before a reset in the CSS file, it wins within its layer. The physical position in the code is irrelevant. What counts is the declared layer order.

A Simple Example

@layer base {
  .button {
    background: #ccc;
  }
}

@layer components {
  .button {
    background: blue;
  }
}

Result:

The component definition wins, even if it stands above the base layer in the code. This is the fundamental difference to traditional CSS. The layer determines priority, not the physical position in the file content or the specificity of the selector. Both .button rules have the same specificity, one class, but the components layer was declared after the base layer and therefore automatically wins.

Declaring and Filling Layers

There are two common ways to use layers, and both can be freely combined.

The first variant is to declare layers first and fill them later. This makes particular sense when you want to establish the layer order at the beginning of the stylesheet, but the actual styles are defined in different files or sections. You declare all layers in the desired order and can then later, even in arbitrary code order, fill them with content.

The second variant is to declare a layer directly when first using it and fill it simultaneously. This is practical for smaller projects or when introducing layers incrementally. You write @layer components { ... } and define the styles directly. The layer is automatically registered in the order in which it first appears.

Both variants can be mixed without problems. You can declare the most important layers at the beginning to secure the order, and later add more layers as needed. The crucial point: The order is determined by the first mention of a layer, not the last.

Examples for Both Variants

Variant 1: Declare Layer and Fill Later

@layer reset, base, components, utilities;

/* sometime later, perhaps in a different file */
@layer base {
  body {
    margin: 0;
  }
}

Variant 2: Fill Layer When Declaring

@layer components {
  .card {
    padding: 1rem;
  }
}

Example: Design System + Project Styles

In large projects, overriding is a common problem. A central design system defines basic components, but individual projects or pages require specific adjustments. Without layers, this often leads to specificity battles: project styles must be more specific than design system styles, leading to nested selectors or !important usage.

With layers, this is solved elegantly. You define a clear hierarchy that explicitly states that project overrides should take precedence over design system components.

Example structure:

@layer reset, theme, components, overrides;

reset

This layer contains CSS Resets, Normalize, or other baseline rules. Here, browser defaults are neutralized. This includes things like margin: 0 on the body, box-sizing: border-box globally, or removing default styles on lists. These rules deliberately have the lowest priority, as they only create a foundation and should be able to be overridden by all other layers.

theme

In this layer live design tokens. Color definitions, font sizes, spacing, border radii, shadows, and all other reusable values. Here, CSS Custom Properties are defined that all later layers access. This layer contains no concrete component styles, only the design language of the project. It takes precedence over resets, but not over components.

components

Here live the component styles of the design system, such as buttons, cards, forms, navigation, modals. Each component accesses the design tokens from the theme layer but defines its own visual structure. This layer is the heart of the design system and contains the majority of reusable styles.

overrides

This layer is intended for project-internal or page-related adjustments. When a page needs to style a component slightly differently, it happens here. The overrides layer has the highest priority and can consciously override design system components without having to increase specificity.

With this, one thing is clear. Overrides beat components, components beat theme, theme beats reset. Completely without high specificity or !important. The architecture is self-documenting because the layer names explicitly state what they are intended for.

Another Case

A team worked on a multi-brand platform with a central design system and various microfrontends. Each microfrontend represented a different brand or business area and required its own CSS overrides to make brand-specific adjustments. The design system provided the base components such as buttons, forms, cards, but each microfrontend had to be able to visually adapt these components.

The problem with this is as follows. Without layers, this led to a constant battle over specificity. The design system components had a certain specificity, and the microfrontends had to make their overrides more specific to override them. This led to selectors like .brand-a .button.primary, .mf-checkout #header .button, or direct !important usage. Each adjustment made the CSS more fragile, and conflicts between different microfrontends were hard to trace. Maintenance became increasingly complex, and new features took longer because regression tests had to be run constantly.

After introducing layers, the structure was radically simplified:

@layer ds-reset, ds-base, ds-components, mf-overrides;

The design system layers (ds-reset, ds-base, ds-components) defined the foundation. The mf-overrides layer was reserved for all microfrontends and had guaranteed highest priority. This allowed microfrontend developers to write simple selectors like .button { background: var(--brand-color); } without worrying about specificity. The mf-overrides layer automatically won against all design system components.

The result: Microfrontend overrides were guaranteed dominant, components remained stable and the entire system became predictable. The code became cleaner and more maintainable. Conflicts between microfrontends were a thing of the past because each microfrontend placed its overrides in the same layer and they didn’t override each other. Development speed increased noticeably because developers could focus on functionality instead of debugging CSS specificity.

Layers + Utility Classes

Utility classes are a controversial topic in the CSS world, but they have established themselves in many modern projects. Frameworks like Tailwind CSS, Open Props, or custom utility sets offer atomic classes for common styling tasks like margin, padding, flexbox properties, or text formatting. The problem: Utility classes often collide with component styles, and traditionally you have to give them aggressive specificity or !important to make them prevail.

With layers, this is solved elegantly. You explicitly define that utilities should have the highest priority, and this happens purely through the layer order:

@layer components, utilities;

Utilities always win and that without having to give them aggressive specificity. A simple .text-white { color: white; } in the utilities layer beats any specific selector in the components layer, no matter how specific. This is the philosophy of utility-first: utilities should be dominant because they represent conscious overrides.

Example:

@layer components {
  .button {
    color: black;
  }
}

@layer utilities {
  .text-white {
    color: white;
  }
}

HTML:

<button class="button text-white">Click</button>

Without layers, .button would win because both selectors have the same specificity and .button comes first in the HTML. With layers, .text-white wins because the utilities layer was declared after the components layer. And all this without specificity battles or !important.

This makes utility-first approaches significantly cleaner and more maintainable. You can use utilities without overloading the entire design system with !important, and components still remain clearly defined.

Layers + Frameworks (React, Vue, Astro, etc.)

Modern frontend frameworks have an inherent CSS problem. They mix styles from various sources, and the order is often hard to predict. Global files are loaded before components are rendered. Page components can have their own styles. UI components bring their own styles. And bundlers like Webpack, Vite, or Turbopack generate from all this one or more CSS bundles whose order can vary depending on build configuration.

This leads to a classic problem: A global component style overrides a page-specific override, even though that wasn’t intended. Or a utility class loses against a deeply nested framework selector. The solution so far was: increase specificity, use !important, or write styles inline in the markup. All not good solutions.

With layers, importance can be clearly defined, regardless of the order in which the bundler assembles the files:

@layer reset, base, components, page, utilities;

This structure makes explicit that page-specific styles take precedence over global components. Utilities consciously overwrite everything. There is no danger from file order because the layer order guarantees the behavior.

A concrete example of this would be as follows. In an Astro project, you have global component styles in src/styles/components.css, page-specific styles in src/pages/about.astro, and utility classes in src/styles/utilities.css. Without layers, it’s unclear which styles win because Astro merges the files depending on import order and build optimizations. With layers, it’s guaranteed: page styles beat components, and utilities beat everything.

This creates safety in the team and especially in multi-brand or microfrontend projects where different teams work in parallel. Everyone knows in which layer their styles should live, and there are no surprises.

Best Practices for Layers

1. Use Only Few, Clear Layers

The temptation is great to define a separate layer for every use case. But too many layers make the system confusing and difficult to maintain. If you have ten or more layers, you have to first ask yourself with every new rule which layer it belongs to. This is counterproductive.

A proven recommendation is to limit yourself to four to six layers: reset for browser resets and baseline rules, base for basic element styles, theme for design tokens and custom properties, components for reusable UI components, overrides for project-specific adjustments and utilities for atomic helper classes. This structure covers the vast majority of use cases and remains manageable.

2. Define Layers Early

Layers should ideally be declared at the very beginning of the global stylesheet, even before the first style is written. This makes the architecture immediately visible and ensures that all subsequent styles can be properly categorized. A declaration like @layer reset, base, theme, components, overrides, utilities; at the beginning of global.css documents the entire layer structure at a glance.

3. Keep Specificity Low Anyway

@layer is not a substitute for good CSS architecture, but a strong addition. You should still be careful to keep specificity low, use semantic classes, and avoid nested selectors. Layers solve the problem of priority between different style sources, but they don’t exempt you from writing clean, maintainable code. A layer full of highly specific selectors is still fragile and difficult to maintain.

4. Components Should Live in Own Layers

Design systems benefit enormously when all component styles live in a dedicated components layer. This makes it easy to override components globally (in the overrides layer) or supplement them with utilities (in the utilities layer). The components themselves remain stable and predictable.

5. Combine Layers + Custom Properties

The combination of layers and CSS Custom Properties is particularly powerful. You define all design tokens in the theme layer as custom properties and use them in all subsequent layers. This creates a clean, flexible system where the design can be centrally adjusted without having to touch component code. Example: @layer theme { :root { --primary: #0055ff; } } defines the primary color, and all components access it.

A Small, Complete Example

@layer reset, theme, components, overrides;

/* Reset */
@layer reset {
  *, *::before, *::after {
    box-sizing: border-box;
  }
}

/* Theme */
@layer theme {
  :root {
    --brand: #0055ff;
  }
}

/* Components */
@layer components {
  .button {
    padding: 0.75rem 1rem;
    background: var(--brand);
    color: white;
  }
}

/* Overrides */
@layer overrides {
  .button {
    background: black;
  }
}

Without layers, you would have to write a class with higher specificity or use !important. With layers, it is a cleanly defined architectural decision that explicitly documents that overrides take precedence over components.

Browser Support and Compatibility

Cascade Layers are supported by all modern browsers:

Browser support is therefore very good, as long as you don’t need to support very old browsers. For projects with legacy browser support, there are two approaches:

Feature Detection with @supports:

@supports (at-rule(@layer)) {
  /* Modern styles with layers */
  @layer components {
    .button { background: var(--primary); }
  }
}

/* Fallback without layers */
.button {
  background: #0055ff;
}

Note: Feature detection for @layer is currently not perfectly implemented. A pragmatic alternative is to check browser versions or use progressive enhancement, where layers serve as an additional organizational level, but functionality is also provided without them.

Migration: Introducing Layers into Existing Projects

Introducing layers into an existing project should be done gradually. A big-bang refactoring is risky and often unnecessary.

Step 1: Analyze Structure

First, you should understand the existing CSS structure. Where do styles come from? Are there global resets? How are components organized? Where are utilities used? This analysis helps define the right layer names.

Step 2: Declare Layers

Next, you define the layer order at the beginning of the global stylesheet file. You don’t need to move styles yet, but first establish the structure:

@layer reset, base, theme, components, page, utilities;

Step 3: Migrate Gradually

Now begins the actual migration. Ideally, you start with the resets, as these have the lowest priority and cause the fewest conflicts. Then follows the theme layer with design tokens. After that, you migrate components, then page-specific styles, and finally utilities.

Important: Unlayered styles (styles outside any @layer) have the highest priority and override all layers. This means: You don’t have to migrate everything immediately. Existing code continues to work and even takes precedence over all layer styles. This enables a gradual migration where you introduce layer by layer without causing regressions.

Step 4: Testing and Validation

After each migration phase, you should test thoroughly, especially visual regression tests with tools like Percy, Chromatic, or Playwright. Layers change the Cascade priority, and even if the migration was performed cleanly, subtle differences can occur.

Conclusion

Cascade Layers are one of the most important CSS features for large projects. They make styling predictable, reduce specificity battles, and offer a clear hierarchy that works independently of the file structure.

For design systems, frameworks, and teams, @layer is an enormous relief and a central building block of modern CSS architecture.


☕ 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 15 – Modern Typography in CSS
Next Post
Door 13 – CSS Architectures. BEM, ITCSS, CUBE CSS Compared

Comments