AniUI Academy

Cascade Layers

@layer decides the order in which groups of CSS rules apply — reset, design system and app code each live in a layer whose priority is set once, not fought with specificity bumps.

10 min read

Every real codebase eventually hits the same situation: a button in the design system was styled by one team, the marketing page's hero button was styled by another team with a slightly more specific selector to "win," and three months later nobody is sure which selector a page is picking up without opening the browser inspector. The underlying problem isn't bad CSS — it's that specificity has been the only tool available for deciding "which group of rules wins when they conflict," and specificity is a local, ad-hoc game.

Cascade layers (@layer) are the tool for solving that at the group level: declare the order between whole bodies of CSS once, and stop fighting about it.

The cascade, with one new step inserted

Earlier in the track, the cascade's tiebreakers were: origin, importance, specificity, source order. Layers slot in between origin and specificity. Simplified for author CSS:

  1. Step 1

    1. Origin and importance

    Author rules beat user-agent (browser default) rules; !important flips several of these comparisons.

  2. Step 2

    2. Cascade layer order

    Normal rules in a later-declared layer beat normal rules in an earlier-declared layer. Un-layered rules act as if they were in an implicit layer that comes after every named one. !important flips this: important rules in earlier layers beat important rules in later ones.

  3. Step 3

    3. Specificity

    Only used to decide between rules in the same layer. Specificity no longer competes across layers.

  4. Step 4

    4. Source order

    If everything above ties, the later rule wins.

The counter-intuitive piece — worth staring at for a second — is that layer order is checked before specificity. A plain button in a later layer beats a main .sidebar nav a.primary in an earlier layer, regardless of specificity. That's the behaviour the feature exists for.

Declaring the order once

The one line that does the real work is a bare @layer statement listing the layers in the order you want:

@layer reset, base, components, utilities;

That single line establishes four layers and fixes their relative order. Anything you later write inside one of them — in this file or any other — slots into the layer at that priority:

@layer reset {
  * { margin: 0; padding: 0; box-sizing: border-box; }
}
 
@layer base {
  body { font-family: system-ui; line-height: 1.5; }
  a { color: inherit; }
}
 
@layer components {
  .button {
    padding: 0.5rem 1rem;
    border-radius: 0.5rem;
    background: var(--brand);
  }
}
 
@layer utilities {
  .sr-only {
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
  }
}

A utility class in utilities will win over a component rule in components, which wins over a base rule in base. Specificity stops being the arbiter; the layer declaration is.

The implicit un-layered layer

CSS not written inside an @layer block doesn't vanish — it goes into an implicit, unnamed group that ranks after every named layer for normal declarations. In practice:

reset → base → components → utilities → (un-layered CSS)

This is the detail that makes incremental adoption realistic. You can move your reset and your design system into layers without touching the rest of the application's CSS, and the application CSS continues to behave exactly as it did before, because it still takes last word.

!important flips the order

Important rules follow the reverse layer order. In the layer declaration above, an !important rule in reset would beat an !important rule in utilities.

This sounds strange until you see what it's for: the browser's user-agent styles are a layer that comes before every author layer, and !important in the user layer (user stylesheets, browser accessibility overrides) is specifically designed to override author styles. The same logic applies to your own layers: an "emergency override" rule you mark important in your reset layer can override an important rule in your utilities, which is the escape hatch you want when a specific component is behaving badly.

Most well-structured code never needs this. It's useful to know it's there, so an !important surprise in a reset layer isn't a mystery.

Layering a vendor stylesheet at import

The practical trick that retires most of the "how do I override Bootstrap" category of problems:

@import url("https://cdn.example.com/vendor.css") layer(vendor);
 
@layer vendor, app;
 
@layer app {
  .my-button {
    background: var(--brand);
  }
}

Every rule in the vendor file is placed inside the vendor layer. The layer declaration puts vendor before app. Anything you write inside @layer app beats anything in vendor, regardless of how specific the vendor selectors are. No !important sprinkling, no .specific .more-specific .please selector cosplay.

The same shape works for any third-party CSS you'd otherwise have to fight: a date picker, an admin UI kit, a markdown style sheet shipped by a library.

When not to reach for layers

Layers are infrastructure, not a tool for a single component. Two anti-patterns:

  • Too many layers. Reset, base, components, utilities is a reasonable starting shape. 15 named layers for a team of three is a burden to reason about.
  • Using a layer to hide a specificity problem inside one file. If one component's internal rules are competing, that's usually a sign that the component needs refactoring, not that it needs its own cascade layer.

The feature pays off at the level of "whole bodies of CSS whose relative priority I want to lock in." That's where to use it, and only there.

What to remember

  • Cascade layers (@layer) decide the order between groups of CSS rules before specificity is consulted. A plain selector in a later layer beats a specific selector in an earlier layer.
  • Declare the order once with a bare @layer a, b, c; statement. Everything subsequently assigned to one of those layers takes its place.
  • Un-layered CSS acts as an implicit layer that comes after every named layer. This is the migration-friendly default.
  • !important flips the order: important rules in earlier layers beat important rules in later ones. Useful for intentional escape hatches; surprising if you don't know it.
  • @import url(...) layer(name) pins a third-party stylesheet into a layer, so your own code can override it without specificity wars.
  • Use layers to settle "which body of rules wins" at the architecture level. Don't reach for them to resolve a specificity problem inside a single component.

Check yourself

4 questions · pass 3/4 to unlock The Box Model

up to 50
  1. 1.In the cascade order, how do cascade layers rank relative to specificity and !important?

  2. 2.Where does CSS that isn't inside any @layer block live in the cascade?

  3. 3.What's the clearest reason to prefer @layer over the long-standing 'just add another class selector' trick to boost specificity?

  4. 4.A common pattern is to put a third-party stylesheet into its own layer so your app code can override it cleanly. What's the one-line way to do that at import time?

4 left to answer

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.