
How to Use CSS Cascade Layers (@layer) to Tame Specificity
Every CSS codebase eventually hits the same wall. A third-party library ships a selector like .datepicker .header button.nav, and you need to override it. So you write something even more specific. Then someone needs to override your override, and out comes !important. A few months later, nobody can change a button color without opening DevTools to figure out which of six competing rules is winning.
Cascade layers were designed to end this arms race. With @layer, you decide up front which groups of styles take priority, and that order beats specificity. A single class in a higher layer overrides an ID selector in a lower one. This guide explains how layers fit into the cascade, how to set them up, and how to use them with resets, frameworks, and third-party CSS.
Where Layers Fit in the Cascade
When multiple declarations target the same property on the same element, the browser resolves the conflict by checking criteria in order. Simplified, that order is:
- Origin and importance (user agent, user, author styles, and whether
!importantis used) - Context (for example, styles inside shadow DOM)
- Element-attached styles (the
styleattribute) - Cascade layers
- Specificity
- Order of appearance
The key insight is that layers are checked before specificity. Specificity only breaks ties between declarations in the same layer. If two rules live in different layers, the layer order decides, no matter how specific the selectors are.
Creating Layers
There are several ways to put styles into a layer.
Block syntax
@layer components {
.button {
padding: 0.5rem 1rem;
border-radius: 6px;
}
}
Declaring layer order up front
The order in which layers are first declared determines their priority. Later layers win over earlier ones. The cleanest approach is to declare all your layers in one statement at the very top of your CSS:
@layer reset, base, vendor, components, utilities;
Now utilities beats components, which beats vendor, and so on, regardless of where in the file (or in which file) each layer's rules actually appear. You can add rules to a layer as many times as you like; they all merge into the same layer.
@layer reset, base, components, utilities;
@layer utilities {
.mt-0 {
margin-top: 0;
}
}
@layer components {
.card {
margin-top: 2rem;
}
}
Even though utilities rules appear first in the file, .mt-0 wins because the utilities layer was declared after components in the ordering statement.
Importing into a layer
You can place an entire stylesheet into a layer with @import:
@import url("vendor/datepicker.css") layer(vendor);
@import url("reset.css") layer(reset);
This is the most useful feature for taming third-party CSS. The library's selectors can be as specific as they like; they're all trapped in the vendor layer, below your components.
Remember that @import rules must appear before other rules in a stylesheet, except for @charset and layer ordering statements (@layer a, b;), which are allowed before them.
Anonymous layers
You can create a layer without a name:
@layer {
.legacy-widget {
color: #334155;
}
}
Anonymous layers can't be referenced later, so you can't add more rules to them or position them in an ordering statement. They're occasionally useful for isolating a chunk of styles, but named layers are almost always better.
Unlayered Styles Win
Here's the rule that surprises people most: styles that aren't in any layer beat all layered styles (for normal, non-important declarations). Unlayered CSS is treated as an implicit final layer.
@layer components {
#main .card.featured {
background: #e0f2fe;
}
}
.card {
background: #ffffff; /* This wins, because it's unlayered */
}
This design is intentional. It means you can adopt layers gradually. Put your reset and third-party CSS into layers, leave the rest of your existing code unlayered, and the existing code automatically wins without changes.
It also means that if you move everything into layers except one stray stylesheet, that stylesheet will override everything. Be deliberate about which styles are unlayered.
A Practical Layer Architecture
Here's a structure that works well for most projects:
/* main.css */
@layer reset, base, vendor, layout, components, utilities, overrides;
@import url("reset.css") layer(reset);
@import url("vendor/carousel.css") layer(vendor);
@layer base {
:root {
--color-text: #0f172a;
--font-body: system-ui, sans-serif;
}
body {
font-family: var(--font-body);
color: var(--color-text);
line-height: 1.6;
}
a {
color: #2563eb;
}
}
@layer layout {
.container {
max-width: 72rem;
margin-inline: auto;
padding-inline: 1rem;
}
}
@layer components {
.button {
display: inline-flex;
align-items: center;
padding: 0.625rem 1.25rem;
border-radius: 6px;
background: #0f172a;
color: #fff;
}
.carousel .carousel-button {
background: #0f172a; /* beats any vendor selector */
}
}
@layer utilities {
.hidden {
display: none;
}
.text-center {
text-align: center;
}
}
The purpose of each layer:
- reset: normalizing browser defaults. Lowest priority so anything can override it.
- base: element-level defaults and design tokens.
- vendor: third-party libraries.
- layout: page structure, containers, grids.
- components: your UI components.
- utilities: single-purpose helper classes that should always win over components.
- overrides: an escape hatch for rare, deliberate exceptions.
Nested Layers
Layers can be nested, which is handy for organizing large systems or scoping a library's internal structure:
@layer components {
@layer base, variants;
@layer base {
.button {
background: #0f172a;
}
}
@layer variants {
.button.secondary {
background: #e2e8f0;
}
}
}
You can refer to a nested layer with dot notation:
@layer components.variants {
.button.danger {
background: #dc2626;
}
}
Nested layers are ordered within their parent. components.variants beats components.base, but everything in components still sits below utilities.
Layers and !important
This is where layers get counterintuitive. For !important declarations, the layer order is reversed. An important declaration in an earlier layer beats an important declaration in a later layer, and important layered styles beat important unlayered styles.
@layer reset, components;
@layer reset {
[hidden] {
display: none !important;
}
}
@layer components {
.panel {
display: flex !important; /* loses to the reset's !important */
}
}
That reversal is deliberate. It lets low-level layers like a reset protect critical rules (like making [hidden] actually hide things) from being accidentally overridden by higher layers. It mirrors how user !important styles beat author !important styles in the origin system.
The practical takeaway: with layers in place, you should almost never need !important for ordinary overrides. Reserve it for rules that must be protected, and put those in low layers.
Working with Frameworks and Utility CSS
Layers are particularly helpful with utility frameworks. Tailwind CSS v4 uses native cascade layers internally (theme, base, components, utilities), which is why its utilities reliably override component styles. If you're writing custom CSS alongside Tailwind v4, put it into the appropriate layer so it doesn't accidentally beat utilities by being unlayered:
@import "tailwindcss";
@layer components {
.prose-callout {
border-left: 4px solid var(--color-sky-400);
padding: 1rem;
}
}
Now a utility class like p-8 on a .prose-callout element overrides the component padding, as you'd expect. If you'd left .prose-callout unlayered, it would win over every utility.
For libraries that don't use layers, such as an older component library or a CSS file you can't edit, import it into a low layer:
@layer vendor, app;
@import url("https://cdn.jsdelivr.net/npm/some-library/dist/styles.css")
layer(vendor);
Debugging Layers in DevTools
All major browser DevTools show layer information in the Styles panel. Rules are grouped under their layer name, and Chromium-based browsers also offer a layer view that displays the full layer order. If a rule isn't winning and specificity looks right, check which layer each rule belongs to. Nine times out of ten, the "losing" rule is in a lower layer or the "winning" rule is unlayered.
Common Pitfalls
Forgetting the ordering statement. Without @layer reset, base, components; at the top, layer order is determined by whichever layer appears first in the source. If files are bundled in a different order, priorities change silently. Always declare the order explicitly, and make sure that declaration is loaded first.
Leaving critical styles unlayered by accident. A component stylesheet that someone forgot to wrap in @layer will beat everything else. If you're fully layered, consider a lint rule or a code review check.
Expecting layers to change inheritance. Layers only affect how conflicting declarations on the same element are resolved. They don't change inheritance. An inherited value from a parent always loses to any declaration directly on the child, regardless of layer.
Misusing !important inside layers. Because the order reverses for important declarations, sprinkling !important into a high layer won't win against an important rule in a low layer. That's usually a sign you should remove both.
Specificity still matters within a layer. Layers don't eliminate specificity; they scope it. Two conflicting rules inside components are still resolved by specificity and then source order. Keeping selectors flat inside each layer is still good practice. Pairing layers with :where() for zero-specificity defaults works well:
@layer base {
:where(ul, ol)[role="list"] {
list-style: none;
padding: 0;
}
}
Browser Support
Cascade layers are supported in all current major browsers and have been for several years. Browsers that don't understand @layer ignore the whole block, meaning the styles inside simply don't apply, so for very old browsers there isn't a graceful degradation path within the same file. For nearly every audience today this isn't a concern, but if you must support legacy browsers, build tools like PostCSS can flatten layers into equivalent specificity-based CSS.
Conclusion
Cascade layers give you explicit control over the cascade. Instead of escalating specificity or reaching for !important, you declare an order of layers once, and styles in later layers win regardless of how specific earlier selectors are. Resets and vendor CSS go into low layers where they can't cause trouble, components sit in the middle, and utilities stay on top.
Start by adding one line to the top of your stylesheet, an ordering statement like @layer reset, vendor, components, utilities;, and move your reset and third-party CSS into their layers. Leave everything else unlayered for now. You'll immediately gain the ability to override libraries with simple selectors, and you can layer the rest of your codebase at your own pace.


