Cascade Layers for Reset and Design Tokens
A stylesheet has two kinds of foundation that behave nothing alike. A reset exists to be overridden — its whole job is to flatten user-agent defaults and then get out of the way. Design tokens exist to be consumed — every layer above reads them, and the ability to redefine one for a theme or a density mode without touching a single component is the point of having them. Putting both into an undifferentiated blob of top-of-file CSS is why resets creep back into components and why theming turns into a search for the selector that will finally win. The @layer rule lets you give each foundation an explicit position and an explicit shape. This guide, part of the modern CSS reset strategies collection within Mastering Container Queries & Responsive Layouts, covers how to structure those two layers and their sublayers. The broader question of how layer order relates to selector weight is handled separately in cascade layers vs specificity hacks.
Problem statement
Two concrete failures motivate this. The first: a reset sets ul { list-style: none } and, three months later, a content component needs real bullets back — so someone writes .prose ul { list-style: disc }, which works, and then someone else nests a list inside a card and the reset wins again, and the escalation begins. The second, and more expensive: a product needs a dark theme and a compact density mode. The tokens were declared on :root, so overriding them means finding a selector that beats :root, which produces html[data-theme="dark"]:root and worse. Both failures are the same failure — a foundation with no declared position in the cascade — and both are fixed by naming the position rather than fighting for it.
Approach rationale: two foundations, two jobs, two layers
Reset goes first because it is the most overridable thing in the system. Anything in an early layer loses to everything above it for normal declarations, which is exactly the contract a reset wants: apply everywhere, defend nothing. That freedom is what lets a modern reset use whatever selectors read most clearly, including element and descendant selectors, without any downstream cost.
Tokens go second, in their own layer, and the reason they are not merely part of reset is that they are read rather than overridden. Components consume var(--space-3); they do not compete with it. Giving tokens their own named layer means you can slot theme and density variants inside that layer as sublayers, keeping every value that defines the look of the product in one addressable region of the cascade, above the reset and below everything that uses it.
The alternative approaches are worse in familiar ways. Preprocessor variables resolve at build time and cannot respond to a runtime theme attribute or a media query at all. A single flat :root block with attribute-selector overrides works but forces every theme rule to out-specify the base, so the base can never be made more specific later without breaking every theme. Layers make the override relationship structural: tokens.theme beats tokens.base because you said so once, at the top of the file.
Complete working implementation
This self-contained file declares the layer order once, fills the reset and tokens layers properly, and shows a component consuming tokens. Nothing below the token layer uses a literal colour or spacing value.
<!doctype html>
<html lang="en" data-density="comfortable">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
/* Declare the order ONCE, up front. Every layer's rank is fixed here,
regardless of where its rules are physically written later. */
@layer reset, tokens, components, utilities;
/* ================= RESET =========================================
Only user-agent normalisation lives here. No design decisions,
no colours, no spacing values — those are tokens. */
@layer reset {
*, *::before, *::after { box-sizing: border-box; }
/* Remove default margins; spacing becomes a deliberate token. */
body, h1, h2, h3, p, figure, blockquote, ul, ol, dl, dd {
margin: 0;
}
/* Media defaults to block so it stops sitting on the text baseline,
and never overflows its column. */
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
/* Form controls do not inherit typography from the page by default. */
input, button, textarea, select {
font: inherit;
color: inherit;
}
/* Lists used for navigation lose their markers; content lists keep
theirs, because .prose sits in a later layer and simply wins. */
ul[role="list"], ol[role="list"] {
list-style: none;
padding: 0;
}
body { min-height: 100svh; line-height: 1.5; }
}
/* ================= TOKENS ========================================
Sublayers declared in order: base values, then theme overrides,
then density overrides. A later sublayer redefining a token wins
on layer order alone — custom property declarations participate
in the cascade exactly like any other declaration. */
@layer tokens {
@layer base, theme, density;
@layer base {
:root {
--color-bg: #ffffff;
--color-fg: #14181f;
--color-accent: #2b5fd9;
--color-muted: #5b6577;
--space-1: 0.5rem;
--space-2: 1rem;
--space-3: 1.5rem;
--radius: 8px;
--font-ui: system-ui, sans-serif;
}
}
@layer theme {
/* Same selector weight as base. It wins because density and theme
are later sublayers, not because the selector is heavier. */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0d1017;
--color-fg: #e8ecf4;
--color-accent: #7aa2ff;
--color-muted: #99a3b8;
}
}
}
@layer density {
[data-density="compact"] {
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
}
}
}
/* ================= COMPONENTS ====================================
Consumes tokens only. Wins over reset by layer order. */
@layer components {
body {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-ui);
padding: var(--space-3);
}
.card-shell { container-type: inline-size; container-name: card; }
.card {
display: grid;
gap: var(--space-2);
padding: var(--space-3);
border-radius: var(--radius);
border: 1px solid color-mix(in srgb, var(--color-fg) 15%, transparent);
}
.card a { color: var(--color-accent); }
/* Content lists get their markers back. This beats the reset purely
because components is a later layer. */
.prose ul { list-style: disc; padding-inline-start: var(--space-3); }
/* The container query lives in the SAME layer as the component it
adjusts, so its overrides rank with that component. */
@container card (min-width: 480px) {
.card { grid-template-columns: 1fr 2fr; align-items: start; }
}
}
/* ================= UTILITIES =====================================
Last, so one class can override a component when it must. */
@layer utilities {
.text-muted { color: var(--color-muted); }
.stack-tight { gap: var(--space-1); }
}
</style>
</head>
<body>
<div class="card-shell">
<article class="card prose">
<h2>Layered foundations</h2>
<ul>
<li>Reset normalises, tokens decide, components consume.</li>
<li>Theme and density are sublayers of tokens.</li>
</ul>
<p class="text-muted"><a href="#">Read the full note</a></p>
</article>
</div>
</body>
</html>
Key technique: token sublayers resolve theming without selectors
The decisive detail is @layer base, theme, density; nested inside @layer tokens. A custom property declaration is an ordinary declaration — it goes through the whole cascade, including layer sorting — so --space-2 declared in tokens.density beats --space-2 declared in tokens.base even though both are written against selectors of comparable weight. That single fact removes the entire category of theming problems where an override has to be made heavier than the thing it overrides.
The practical consequences are worth spelling out. You can keep every theme's token block on the plainest selector that expresses its trigger, :root under a media query or a single attribute selector, because ordering, not weight, decides the winner. You can add a fourth sublayer later — a per-brand tokens.brand, say — by extending one line, and it slots above the others without touching any existing rule. And because the whole tokens layer moves as a unit, a component can never accidentally shadow a token by being more specific: components sit in a later layer entirely, so if they redefine a token they are meant to.
Variation: an imported reset and a scoped component library
Two extensions matter once the system leaves a single file. First, a third-party reset can be pulled directly into your reset layer with @import ... layer(), so it inherits that layer's position rather than sitting loose. Second, a component library you consume can be given its own sublayer inside components, keeping its rules below your own overrides.
/* The vendor reset lands in your reset layer, at the bottom of the stack.
@import must appear before all other rules except @charset and @layer. */
@import url("modern-normalize.css") layer(reset);
/* A library gets a sublayer below your own component code. */
@layer components {
@layer lib, app;
@layer lib { /* third-party component CSS is written or imported here */ }
@layer app { .card { padding: var(--space-3); } }
}
/* Tokens can also be scoped rather than global, which is useful for a
component that must carry its own palette wherever it is placed. */
@layer tokens {
@layer base {
.invert-surface {
--color-bg: var(--color-fg);
--color-fg: #ffffff;
}
}
}
The scoped-token pattern at the end is the one people underuse. Because custom properties inherit, redefining them on a wrapper element re-points every var() reference inside that subtree without any component knowing it happened — a card inside .invert-surface renders inverted with no extra class, no modifier, and no component-level rule. Tokens defined this way pair naturally with cross-area animation work, such as fluid spacing tokens driving transition durations, where the same value feeds both a layout gap and a motion duration.
Browser support note
Cascade layers shipped in Chrome and Edge 99, Firefox 97, and Safari 15.4, all in the first half of 2022, so @layer is safe on every current engine in 2026. Nested sublayers and the @import url(...) layer(name) form landed in the same releases, with no separate rollout to check. Custom properties themselves are far older and supported everywhere layers are. color-mix(), used in the component layer above, needs Chrome and Edge 111+, Firefox 113+, or Safari 16.2+, so substitute a literal colour if your floor is lower. There is no meaningful graceful-degradation story for @layer itself: an engine that does not recognise the at-rule discards everything inside it, so a layered stylesheet served to a pre-2022 browser renders unstyled rather than degraded. In practice that means shipping layers unconditionally to modern engines and, if you genuinely must support older ones, serving them a separate flattened build.
FAQ
What belongs in the reset layer and what does not?
Only rules that normalise user-agent defaults: box-sizing, margin removal, media element defaults, form control font inheritance. Anything expressing a design decision, including colours and spacing values, belongs in the tokens layer instead.
Do custom property declarations obey cascade layer order? Yes. A custom property declaration is an ordinary declaration and participates fully in the cascade, so a token redefined in a later layer or sublayer wins. This is what makes theme sublayers work without any selector escalation.
Should tokens be their own layer or a sublayer of reset? Their own layer, placed after reset. The reset should be freely overridable by everything, while tokens are consumed by every layer above them, so they are two different jobs with two different positions in the order.
Do container queries need their own layer?
No. A @container rule carries the selectors written inside it, so it belongs in whichever layer those component styles already live. Keeping the component and its container queries in one layer keeps responsive overrides predictable.
Related
- Modern CSS Reset Strategies — the parent guide on building a spec-compliant baseline.
- Cascade Layers vs Specificity Hacks — how layer order interacts with selector weight and unlayered CSS.
- How to Use Container Queries in Production — scoping component queries that live inside the components layer.
- Building a Fluid Space Scale with clamp() — generating the spacing values the tokens layer holds.
- CSS Custom Properties Architecture — cross-area guide on structuring the tokens this layer houses.
Related articles
More pages in the same section.