On a large project, specificity wars are a constant source of bugs. A framework's selector is stronger than yours, so you add a class, then an ID, then !important. Cascade layers fix the root cause: they let you decide which group of styles wins, before specificity is even considered.
Where layers sit in the cascade
Recall the cascade steps from earlier in the course: origin and importance, context, inline styles, layers, specificity, then order of appearance. Layers come before specificity. That is the whole point:
When two declarations are in different layers, the layer order decides. Specificity only matters between declarations in the same layer.
Declaring layers
Start with a statement that names your layers in order, lowest priority first:
@layer reset, base, components, utilities;
Then put rules into layers with a block:
@layer reset {
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
}
}
@layer base {
a {
color: #0b5394;
}
}
@layer components {
.card a {
color: #102a43;
}
}
@layer utilities {
.text-danger {
color: #b42318;
}
}
Later layers in the list beat earlier ones. So this link:
<div class="card"><a class="text-danger" href="/cancel">Cancel order</a></div>
is red. .card a scores (0, 1, 1) and .text-danger only (0, 1, 0), but utilities comes after components in the layer order, so specificity is never compared. You no longer need !important on utility classes.
Layer order is set once
The first time a layer name appears decides its position. That is why you declare the order at the top of your main stylesheet. You can then add rules to any layer, anywhere, in any file, and the order holds:
@layer reset, base, components, utilities;
/* Much later, or in another file */
@layer base {
h1 {
font-size: 2.5rem;
}
}
If you skip the statement, layers are ordered by when they are first used, which is easy to get wrong when files are combined.
Unlayered styles win
Any CSS that is not inside a layer is treated as belonging to an implicit final layer. It beats every named layer:
@layer components {
#main .button {
background-color: blue; /* high specificity, but layered */
}
}
.button {
background-color: green; /* unlayered: wins */
}
This is very useful during migration. You can move old CSS into layers gradually, and new unlayered styles still take priority. It also means a stray unlayered rule can unexpectedly override your whole layer system, so be deliberate.
Layering third-party CSS
This is where layers shine. Import a framework or a component library into a low layer, and your own styles beat it, however specific its selectors are:
@layer reset, vendor, base, components, utilities;
@import url("vendor/datepicker.css") layer(vendor);
Note the rules for @import: it must come before all other rules in the stylesheet, except @charset and @layer statements. Placing the @layer statement first, then the imports, is allowed and is the usual pattern.
You can wrap a framework you load another way by importing it from a small file:
/* layers.css */
@layer reset, framework, components, overrides;
@import url("framework.min.css") layer(framework);
@import url("components.css") layer(components);
Layering a CSS framework is often the cleanest way to customise it. Your component styles can use simple class selectors and still win, without copying the framework's long selectors to match its specificity.
Nested layers
Layers can contain sub-layers:
@layer components {
@layer base, variants;
@layer base {
.button { padding: 0.75rem 1.25rem; }
}
@layer variants {
.button-large { padding: 1rem 1.75rem; }
}
}
/* You can also refer to a sub-layer with a dot */
@layer components.variants {
.button-small { padding: 0.5rem 0.75rem; }
}
Sub-layers are ordered within their parent. The whole of components, with all its sub-layers, still sits between base and utilities.
!important reverses the order
Important declarations work the other way round. For !important, earlier layers beat later layers, and important declarations in any layer beat important unlayered ones.
@layer reset, components;
@layer reset {
[hidden] {
display: none !important;
}
}
@layer components {
.panel {
display: flex !important; /* loses to the reset's !important */
}
}
This is intentional. It lets low-level layers, such as a reset, protect essential rules from being overridden by anything later. The [hidden] example is a good use: elements with the hidden attribute stay hidden even if a component sets display with !important.
The reversal surprises most people the first time. If an !important in a later layer "does not work", this is why. Treat !important inside layers as a tool for protecting foundations, not for winning arguments.
Anonymous layers
@layer { ... } without a name creates an anonymous layer. You cannot add more rules to it later or refer to it, so it is mainly useful for quick experiments. Prefer named layers in real projects.
A practical structure
@layer reset, tokens, base, layouts, components, utilities;
@layer tokens {
:root {
--colour-action: #0b5394;
--space-m: 1rem;
}
}
@layer base {
body {
font-family: system-ui, sans-serif;
line-height: 1.5;
}
}
@layer layouts {
.stack > * + * {
margin-top: var(--space-m);
}
}
@layer components {
.button {
background-color: var(--colour-action);
color: white;
}
}
@layer utilities {
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
}
Each layer has one job, and the order matches how general each group is, from broad defaults to targeted overrides.
Browser support
Cascade layers are supported in all current major browsers. Check the Baseline status on web.dev for your audience. There is no simple fallback: a browser that does not understand @layer ignores the whole block, styles inside it included. If you must support such browsers, layers are not yet an option for critical styles, or you need a build tool that converts them.
Debugging layers in DevTools
Chrome, Edge, Firefox and Safari show the layer name above each layered rule in the Styles pane. Chrome and Edge also offer a layer view (the layers icon in the Styles pane toolbar) listing every layer in order. When a rule unexpectedly loses, check its layer before you check its specificity.
Summary
- Layers are compared before specificity; the later layer wins for normal declarations.
- Declare the order once at the top with
@layer a, b, c;and add rules to layers anywhere. - Unlayered styles beat all layers, which helps gradual migration.
- Import third-party CSS into a low layer with
@import url(...) layer(name);. - For
!important, the order reverses: earlier layers win, which protects foundational rules.