Why the Cascade Needs Better Control

Every CSS author has felt the pain of a style suddenly losing a battle it should have won, or watching a third-party stylesheet trample carefully written rules. The root cause is the cascade’s reliance on specificity and source order to settle conflicts between rules of equal importance. Methodologies like BEM and ITCSS exist largely to keep those conflicts manageable. Cascade layers, now an official candidate recommendation, add a native mechanism for ordering whole groups of rules so authors don’t have to fight specificity on a selector-by-selector basis.

The new @layer at-rule lets authors define their own ordering scheme for collections of styles. This slots into the cascade sorting order between element-attached styles (the style attribute) and specificity, giving authors a way to set priorities across entire stylesheets, frameworks, or third-party code before any specific selector is even evaluated.

Cascade layers are available by default in Chrome Canary/Chromium 99+, Firefox Nightly 97+, and Safari Technical Preview 137+.

Understanding the Battlefield: Origins and Sorting

Before layers make sense, it helps to know how the browser prioritizes styles today. The cascade sorts declarations by several levels, in this order:

  1. Origins and importance
  2. Context (such as shadow DOM)
  3. Element-attached styles
  4. Specificity
  5. Order of appearance (“last one wins”)

Origins are the broadest categories. The browser composites styles from three primary origins: the author origin (the CSS you write), the user origin (browser-provided preferences like default fonts and colors), and the User-Agent origin (the browser’s default styles). Transitions and animations count as origins too, since they create virtual rules while running.

Importance flips the origin order. Normally author styles trump user and User-Agent styles, but !important declarations reverse that, producing this priority:

  1. Important User-Agent origin
  2. Important User origin
  3. Important Author origin
  4. Normal Author origin
  5. Normal User origin
  6. Normal User-Agent origin

When a higher sorting level doesn’t resolve a conflict, the browser moves to the next one. Once cascade layers are supported, they will insert between element-attached styles and specificity.

For most authors, the practical battleground is narrowed to the last two levels. Whether you write CSS by hand, use a preprocessor like Sass, adopt BEM, or work with CSS-in-JS, every style you produce lives in the author origin. That means your conflicts are resolved by specificity and source order alone, which is why approaches like BEM lean heavily on classes for every element to keep specificity predictable and why “last one wins” often decides things in practice. Without these guardrails, codebases drift toward !important declarations and ever-stronger selectors as authors try to force their way out of specificity battles.

Cascade layers move that control up a level: instead of managing conflicts rule-by-rule, you set an order for entire layers of styles and let the cascade handle the rest.

Specificity and Source Order in Action

A quick example shows why specificity and source order create the classic headache. Take a paragraph with a class of royal:

<p class="royal">Lorem, ipsum dolor.</p>

Given these rules:

p {
  color: green;
}

.royal {
  color: royalblue;
}

:first-child {
  color: red;
}

The element selector has the lowest specificity, so it’s out of consideration immediately. The class selector .royal and the pseudo-class :first-child have equal specificity, so the browser falls back to order of appearance. Since the pseudo-class rule comes later, the paragraph renders red.

If you intended the royal class to win every time, your options today are limited:

  • Add !important to the color property, which risks making the style hard to override later.
  • Move the .royal rule after the :first-child rule, which is only a temporary fix if more equal-specificity rules appear.
  • Raise specificity with something like p.royal, which reduces the class’s reusability on other elements.

Each fix trades one problem for another. Layers are designed to remove the need for these trade-offs.

Defining and Ordering Layers

The core syntax is the @layer at-rule. A named block defines a layer and its styles:

@layer base {
  body { ... }
}

You can also assign imported stylesheets to a layer using the layer() function with @import. This is useful for wrapping a framework or third-party script, but note that @import carries a performance cost:

@import url(bootstrap.css) layer(bootstrap);

The most powerful feature is defining layer order up front with an @layer statement, formatted on one line or multiple. This lets you set the full priority scheme in one place at the top of your stylesheet, then add styles to those layers later using the block or import methods. In this example, Bootstrap gets the lowest priority, followed by your base styles, with application styles winning conflicts:

@layer bootstrap, base, application;

@import url(bootstrap.css) layer(bootstrap);

@layer base {
  body {... }
}

The layer order is the priority order. Earlier layers lose to later layers, regardless of specificity within them.

One constraint to keep in mind: the spec requires @import statements to appear before you start declaring @layer. Any @import after a layer is defined will be invalid and ignored, so group all your imports first.

Nested Layers

Layers can be nested inside other layers. To reference a nested layer later, use dot notation with the parent and child names together:

@layer framework {
  @layer reset, base, components;
}

@layer framework.reset { ... }

You can also define the nested structure up front with dot notation:

@layer framework.reset, framework.base, framework.components;

Be careful with naming: reusing a name inside a nested layer does not append to an outer layer of the same name—it starts a fresh layer in the nested context. In this example, the result is a new framework.base layer, not an extension of the outer base layer:

@layer base;

@layer framework {
  @layer base { ... }
}

Anonymous Layers

Names are optional. An unnamed (anonymous) layer can be defined inline, but you can’t add styles to it later, and its position among other layers determines its priority:

@layer { /* rules */ }
@import url(framework.css) layer;

Anonymous layers can be useful for enforcing that all layer definitions live in a single location, or as a “private” layer if you deliberately don’t want later authors to extend or reorder it.

How Specificity Works With Layers

Layer order is the first factor in determining which layered rules win. Later layers take priority over earlier ones, regardless of selector specificity. In the layer stack below, theme outranks reset and base, while utilities wins over everything declared before it.

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

This behavior is deliberate. Layers put cascade control back in the author's hands, which means managing specificity rather than fighting it. A simple selector in a later layer overrides what would historically have been a stronger selector in an earlier layer. The h2 element selector below turns all h2s blue because theme comes after base in the layer order.

@layer base {
  article h2 {
    color: purple;
  }
}

@layer theme {
  h2 {
    color: blue;
  }
}

Inside a single layer, though, classic specificity rules still apply. The same behavior holds for nested layers: if the above layers were nested inside a framework layer, an h2 in framework.theme still overrides an h2 in framework.base.

"A metaphor the CSSWG group used in planning layers was comparing them to PhotoShop layers, where the top layer overrides lower layers, but is also “see through” for parts where it doesn’t apply."

Unlayered Styles Win

The CSS Working Group wanted to provide an upgrade path before layers were fully supported. They settled on making unlayered styles — the styles you're used to writing — always take highest priority. Styles outside any layer will win over layered styles, even if the unlayered rule appears earlier in the stylesheet. In the example below, the paragraph has class blue defined inside the utilities layer, yet the unlayered p selector turns it red.

@layer utilities {
  .blue {
    color: blue;
  }
}

/* wins */
p {
  color: red;
}

Unlayered Styles Within Nested Layers

Inside a layer with nested layers, styles that sit directly in the parent layer (not in a nested one) behave like unlayered styles in cascade terms. In this block, the paragraph stays green because the parent-layer rule outranks the nested-layer rule.

@layer typography {
  p {
    color: green;
  }

  @layer content;
}

@layer typography.content {
  p {
    color: blue;
  }
}

If your nested layers can override each other, put all styles in the parent layer into their own nested layers to be safe.

!important Reverses Layer Order

Marking a declaration !important raises its priority, just as in normal cascading. But within layers, !important reverses the sorting order: competing !important declarations in an earlier layer win over those in later layers. This matches how !important behaves across origins. An !important rule inside a layer also beats an unlayered style.

In this diagram, a code sample shows two layers, theme and utiliies, and an unlayered style. All are modifying the color property of .element and marked as !important. Colored bars help emphasize the layer sort order which for the code sample shown is: !important @layer theme, !important @layer utilities, !important unlayered styles, unlayered styles, @layer utilities, and lastly @layer theme.
In this diagram, a code sample shows two layers, theme and utiliies, and an unlayered style. All are modifying the color property of .element and marked as !important. Colored bars help emphasize the layer sort order which for the code sample shown is: !important @layer theme, !important @layer utilities, !important unlayered styles, unlayered styles, @layer utilities, and lastly @layer theme. (Large preview)

Here, three rules set color on .lead with !important: two inside layers and one unlayered. Because of the reversal, the applied color is green where cascade layers are supported.


/* wins */
@layer theme {
  .lead {
    color: green !important;
  }
}

@layer utilities {
  .lead {
    color: red !important;
  }
}

.lead {
  color: orange !important;
}

To override an !important layered rule, you must declare your layer before that layer and also use !important — or simply remove !important and refactor.

This CodePen demonstrates the rules around specificity within and outside layers plus the effect of !important.

See the Pen [@layer specificity and !important](https://codepen.io/smashingmag/pen/podzPbJ) by Stephanie Eckles.

See the Pen @layer specificity and !important by Stephanie Eckles.

At the time of writing, browser devtools did not yet flag when a style came from a layer, though tooling support was in development.

Pairing Layers With Preprocessor Includes

Sass and LESS let you assemble styles from many files. In Sass, you might build a primary stylesheet with @use:

@use "reset";
@use "theme";

Once layers are well supported, you can wrap each include in its own layer for granular architecture. Instead of editing each file, use the Sass load-css mixin to populate a layer from an include:

@use 'sass:meta';

@layer theme {
  @include meta.load-css('theme');
}

If you ship a framework or design system, you could expose an overrideable $layers list. Looping over the list outputs the corresponding layer styles — assuming your stylesheets are named to match your layers.

@use "sass:meta";
@use "sass:list";

$layers: "reset", "theme" !default;

// Outputs the list of layers
@layer #{$layers};

// Outputs each layer's styles via their include
@each $layer in $layers {
  @layer #{$layer} {
    @include meta.load-css($layer);
  }
}

Practical Use Cases

The browser still downloads the full stylesheet, so layers manage competing definitions but do not reduce file size. Keep that in mind when weighing these scenarios.

Rest Styling and Baselines

Resets and baseline styles are meant to be reset by authors, making them ideal for the first layer. And because layers allow appending later, authors can amend those very same styles without raising specificity. A reset layer that sets font-family to sans-serif could later be updated within a WordPress theme via a settings panel, preserving low specificity instead of adding an inline style.

Framework Overrides

Framework authors generally try to keep specificity low, but custom overrides still require care. Placing a framework in its own layer demotes it relative to your custom layers. If Bootstrap wrapped components in layers, you could override them by reopening the same layer name with your styles. Bootstrap also offers Sass variable customization, but those apply at compile time. Layers give you a post-compilation hook: without attaching user styles to library layers, utility styles risk losing specificity when custom base styles are added as unlayered rules.

Theming and Dark Mode

Layers can be created inside other at-rules like @media. This means theme-related rules don't have to sit next to the originals. Using layers inside a media query keeps the specificity defined by layer order, which is safer for introducing a new theme. Nested @layer can set theme ordering, with layer styles applied inside their associated media queries. Here, a dark theme updates custom properties via prefers-color-scheme, leaving a final user layer open for optional overrides.

@layer theme {
  @layer light, dark, prefers-contrast, forced-colors, user;

  @layer light {
    body {
      --background: #f9f9f9;
      --color: #222;

      background-color: var(--background);
      color: var(--color);
    }
  }
}

@media (prefers-color-scheme: dark) {
  @layer theme.dark {
    body {
      --background: #222;
      --color: #fff;
    }
  }
}

Anywhere you load that optional user theme, it reliably overrides previously set theme layers.

Component Layers

You can create a layer per component, but recall how layer specificity works. Containing all component styles in one layer means they follow normal specificity rules internally. Splitting them into separate layers grants later components higher priority. Layers are not a scoping mechanism; for style encapsulation, watch for native CSS scoping, also authored by Miriam Suzanne.

Element States

State selectors like :disabled and :focus belong in a dedicated layer. Previously, excluding a state required a more complex selector that downstream authors then had to match or beat. For example, styling buttons while excluding disabled ones inflated specificity:

.button:not(:disabled) { ... }

Layers let you define component styles that simpler state selectors can override. These state rules apply broadly to links, buttons, and form elements:

@layer components, states;

@layer components {
  .button {
    --focus-color: rebeccapurple;
    
    background-color: rebeccapurple;
    color: #fff;
  }
}

@layer states {
  :disabled {
    background-color: #ddd;
    color: #999;
  }
  
  :focus-visible {
    outline: 2px solid var(--focus-color, currentColor);
    outline-offset: 2px;
  }
}

A working demo is available in this CodePen:

See the Pen [@layer for element state](https://codepen.io/smashingmag/pen/MWOgmjK) by Stephanie Eckles.

See the Pen @layer for element state by Stephanie Eckles.

The Value of Layers

Small, isolated projects may never need cascade layers. But defining layer order once and appending to layers later in your stylesheet is powerful: it preserves intended specificity without extra architecture. For themes, design systems, and projects built with preprocessors or frameworks, layers let you add and amend styles across many files while keeping order-of-appearance and specificity predictable.

Support Status: What's Blocking Wider Adoption

Most modern CSS features can be introduced progressively. Properties like aspect-ratio or selectors like :is() can be paired with @supports to provide fallbacks. Cascade layers, however, are an exception: they represent such a fundamental change to the cascade that meaningful adoption likely requires a polyfill first.

One key limitation is that @supports doesn't currently detect at-rule support. Even if it did, a detection mechanism wouldn't fully solve the problem, because unlayered styles always take precedence over layered ones — meaning graceful degradation isn't straightforward when layers aren't supported.

That doesn't mean you should wait to experiment. Building test projects with cascade layers now will help you understand the mental model before support matures. As more browsers move to evergreen release cycles, acceptable support for your specific audience may arrive sooner than expected.

Managing Specificity Without Layers

While cascade layers await broader support, two pseudo-classes provide solid alternatives for controlling specificity in current browsers: :is() and :where(). Both accept a list of selectors, but they differ in how specificity is computed.

  • :is() takes the specificity of its most specific argument — for example, :is(.class, #id) gives the whole rule ID-level specificity.
  • :where() always contributes zero specificity for its arguments.

Because of its zero-specificity behavior, :where() is particularly useful for resets and baseline styles that must remain easy to override — especially when those defaults include attribute or state selectors that would otherwise bump specificity and complicate later overwriting.

Consider a common pattern: a list element with role="list" added for assistive technology. Attaching a style reset with that attribute selector raises specificity, forcing you to repeat it whenever you later want to adjust the list's padding or margin. Switching to :where([role="list"]) keeps the selector at zero specificity, allowing simpler component selectors to win later without extra weight.

:where(ul, ol):where([role="list"]) { ... }

For a deeper look at these techniques, Mads Stoumann's article "Don't Fight the Cascade, Control It" on CSS-Tricks covers :is() and :where() as cascade-control tools in detail.

Spec Position and Next Steps

Cascade layers are specified in "CSS Cascading and Inheritance Level 5," which reached Candidate Recommendation on January 13, 2022. No major open issues remain, though resolved ones are tracked in the CSS Working Group's GitHub project. If you encounter problems while experimenting, you can file issues directly for the working group's consideration.

Useful References

  • Bramus Van Damme's in-depth review of cascade layers covers the feature's design and usage.
  • The Candidate Recommendation document is the authoritative source for tracking syntax changes.
  • Miriam Suzanne's original explainer provides useful context and history, though some syntax is outdated.
  • A video update from Miriam Suzanne includes a summary of cascade layers in its first two minutes, with slides and transcript.
  • Una Kravets' video overview demonstrates cascade layers with a live demo.