What are CSS Cascade Layers?

CSS Cascade Layers (often just called layers) are a way to group CSS rules into named buckets and then decide which bucket wins when rules conflict. Think of them like shelves in your CSS closet: the top shelf wins, even if the shirt on the lower shelf is louder, brighter, and has a bigger logo.

Layers solve a classic problem: as your CSS grows (or you include libraries), it becomes harder to control what overrides what. Layers let you set a predictable order like: resetbasecomponentsutilities, so you can stop playing “specificity wars” for sport.

Quick refresher: the normal cascade

Before layers, conflicts were mostly settled by: importance (!important), then specificity, then source order (the later rule wins if specificity is tied).

Let’s see the “later wins” rule in action. Activate each snippet to compare.

.badge {
  background: #222;
  color: white;
}
  
.badge {
  background: #222;
  color: white;
}

.badge {
  background: hotpink;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
  display: grid;
  gap: 12px;
}

.badge {
  display: inline-block;
  padding: 10px 14px;
  border-radius: 999px;
  font-weight: 700;
  border: 2px solid #111;
}
  
I am a badge

In snippet 2, the later .badge rule overrides the earlier one, because specificity is the same and source order breaks the tie.

Learn more about the cascade in the CSS Cascade Interactive Tutorial, and about specificity in the CSS Specificity Interactive Tutorial.

Your first layer

You create layers with @layer. A layer can be declared (just names) and/or defined (a block that contains rules).

  • Declare layer order: @layer base, components, utilities;
  • Put rules inside a layer: @layer components { ... }

Here’s the key idea: Layer order is decided by your layer declarations, not by where rules appear in the file.

@layer base, utilities;

@layer base {
.card {
border-radius: 10px;
background: #f3f3f3;
}
}

@layer utilities {
.card {
border-radius: 999px;
}
} 
@layer base, utilities;

/* Notice: utilities appears first in the file... */
@layer utilities {
  .card {
    border-radius: 999px;
  }
}

/* ...but base still loses because utilities is later in layer order. */
@layer base {
  .card {
    border-radius: 10px;
    background: #f3f3f3;
  }
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
  display: grid;
  gap: 12px;
}

.card {
  border: 2px solid #111;
  padding: 14px;
}
  
Card

Which border-radius wins?

Both snippets produce the same result, even though the blocks are swapped. That’s the magic: layer order beats source order.

Layer order declarations

Declaring layers in one line is the easiest way to set a global policy: “Utilities should always be able to override components.”

Try the snippets below and watch how .button changes.

@layer base, components, utilities;

@layer components {
.button {
background: #111;
color: white;
}
}

@layer utilities {
.button {
background: seagreen;
}
} 
@layer utilities, components, base;

@layer components {
  .button {
    background: #111;
    color: white;
  }
}

@layer utilities {
  .button {
    background: seagreen;
  }
}
  
@layer base, utilities, components;

@layer components {
  .button {
    background: #111;
    color: white;
  }
}

@layer utilities {
  .button {
    background: seagreen;
  }
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
  display: grid;
  gap: 12px;
}

.button {
  border: 2px solid #111;
  padding: 10px 14px;
  border-radius: 12px;
  font-weight: 700;
  display: inline-block;
}
  
Button

Layer order decides which layer wins when selectors conflict. You can “flip the hierarchy” by changing the declaration line.

For most sites, this order is friendly and predictable:

  1. reset (lowest priority)
  2. base (element defaults, typography, general layout primitives)
  3. components (cards, buttons, navbars)
  4. utilities (tiny overrides like .text-center, .radius-0)

You can absolutely rename these, but the pattern is what matters: broad rules low, specific “override tools” high.

Unlayered vs layered: who wins?

Here’s a rule that surprises people (in a good way): Unlayered normal CSS beats layered normal CSS.

So if you have layered CSS for most things, but you write a quick unlayered rule later, that unlayered rule sits “above all layers” (for normal declarations).

@layer components;

@layer components {
.note {
background: #fff3b0;
border-color: #b08900;
}
}

/* Unlayered rule (normal importance) */
.note {
background: #c7f9cc;
border-color: #2d6a4f;
} 
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
}

.note {
  border: 2px solid #111;
  border-radius: 12px;
  padding: 12px 14px;
}
  
Note

Does the layered style win, or the unlayered one?

This behavior is useful when you want layers for your “system CSS”, but still want a simple escape hatch for page-specific tweaks.

Layers vs specificity

Another spicy (and very helpful) rule: Layer order is checked before specificity.

That means a selector with lower specificity in a higher layer can beat a selector with higher specificity in a lower layer. Yes, even an #id. Watch:

@layer base, utilities;

@layer base {
#special {
background: #111;
color: white;
}
}

@layer utilities {
.pill {
background: dodgerblue;
color: white;
}
} 
@layer utilities, base;

@layer base {
  #special {
    background: #111;
    color: white;
  }
}

@layer utilities {
  .pill {
    background: dodgerblue;
    color: white;
  }
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
  display: grid;
  gap: 12px;
}

.pill {
  border: 2px solid #111;
  border-radius: 999px;
  padding: 10px 14px;
  font-weight: 700;
  display: inline-block;
}
  
#special + .pill

Try both snippets. In one order, the utility class wins. In the other, the #id wins. Layer order is the decider.

Beginner-friendly takeaway: layers help you avoid writing overly specific selectors just to “make something win.” You can keep selectors clean and still control priority.

How !important works with layers

!important still does what it always did: it jumps ahead of normal declarations. But layers still matter when you compare declarations with the same importance.

  • Important beats normal (even across layers).
  • Between two important declarations, layer order helps decide which one wins.
@layer base, utilities;

@layer base {
.tag {
background: #111 !important;
color: white !important;
}
}

@layer utilities {
.tag {
background: tomato;
}
} 
@layer base, utilities;

@layer base {
  .tag {
    background: #111 !important;
    color: white !important;
  }
}

@layer utilities {
  .tag {
    background: tomato !important;
  }
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
  display: grid;
  gap: 12px;
}

.tag {
  border: 2px solid #111;
  padding: 10px 12px;
  border-radius: 999px;
  font-weight: 700;
  display: inline-block;
}
  
Important test

In snippet 1, the base layer uses !important, so it wins. In snippet 2, both are important, so the higher layer (utilities) wins.

Practical advice: use !important rarely, but if you must use it, layers help you keep it contained and predictable.

Debugging layers with revert-layer

Layers add a new tool to your debugging kit: revert-layer. It resets a property back to what it would have been without the current layer’s declaration.

Translation: “Ignore what this layer did for this property and fall back to lower layers (or unlayered).”

@layer base, components, utilities;

@layer base {
.panel {
background: #f3f3f3;
}
}

@layer components {
.panel {
background: #ffe8cc;
}
}

@layer utilities {
.panel {
background: #cce3ff;
}

.panel.is-neutral {
background: revert-layer;
}
} 
@layer base, components, utilities;

@layer base {
  .panel {
    background: #f3f3f3;
  }
}

@layer components {
  .panel {
    background: #ffe8cc;
  }
}

@layer utilities {
  .panel {
    background: #cce3ff;
  }
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
  display: grid;
  gap: 12px;
}

.panel {
  border: 2px solid #111;
  border-radius: 14px;
  padding: 12px 14px;
}
  
Normal panel
Neutral panel (tries to undo utilities)

In snippet 1, revert-layer removes the utility layer’s background for .is-neutral, so it falls back to the component layer’s background.

“Why is this not working?” checklist

  • Are both rules in layers? Unlayered normal styles beat layered normal styles.
  • Did you declare layer order? If you don’t, layer order is created by first appearance, which can be confusing in big projects.
  • Are you mixing !important? Important beats normal, even if the normal rule is in a “higher” layer.
  • Are you sure you’re editing the right selector? Layers don’t fix typos (tragically).

Third-party CSS with @import and layer()

A very common use case: you import a library, but you want it to sit in a specific layer. CSS supports putting imported styles into a named layer using: @import url("...") layer(library);

The pattern looks like this:

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

@import url("some-library.css") layer(library);

@layer components {
/* your components */
}

@layer utilities {
/* your utilities that can override library styles */
} 

This lets you “sandwich” a library neatly between your base styles and your utilities, instead of fighting it with extra specificity.

Putting it all together: a mini design system

In this final playground, we’ll use the classic order: resetbasecomponentsutilities. Activate the snippets to see how small utility classes reliably override components without needing stronger selectors.

@layer reset, base, components, utilities;

@layer reset {
button {
border: none;
background: none;
padding: 0;
font: inherit;
color: inherit;
}
}

@layer base {
:root {
color-scheme: light;
}

.app {
font-family: ui-sans-serif, system-ui, sans-serif;
}
}

@layer components {
.button {
background: #111;
color: white;
border-radius: 12px;
padding: 10px 14px;
}

.button.is-danger {
background: #b00020;
}
}

@layer utilities {
.radius-0 {
border-radius: 0px;
}

.bg-ice {
background: #cce3ff;
color: #111;
}
} 
@layer reset, base, components, utilities;

@layer components {
  .button {
    background: #111;
    color: white;
    border-radius: 12px;
    padding: 10px 14px;
  }

  .button.is-danger {
    background: #b00020;
  }
}

@layer utilities {
  .radius-0 {
    border-radius: 0px;
  }

  .bg-ice {
    background: #cce3ff;
    color: #111;
  }

  .button.is-danger {
    background: seagreen;
  }
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.app {
  padding: 16px;
  display: grid;
  gap: 12px;
}

.button {
  border: 2px solid #111;
  font-weight: 700;
  display: inline-block;
}

.row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: center;
}

.hint {
  max-width: 70ch;
}
  

Snippet 1 uses utilities only for small opt-in tweaks. Snippet 2 shows a utility layer overriding a component variant (.button.is-danger) without changing specificity.

Wrap-up cheat sheet

  • Use @layer to group CSS into named buckets.
  • Declare your global order early: @layer reset, base, components, utilities;
  • Layer order is checked before specificity and source order.
  • Unlayered normal CSS beats layered normal CSS.
  • !important still beats normal declarations, but layers still help organize conflicts.
  • Use revert-layer when you want to “undo what this layer did” and fall back to lower layers.