Naming Cascade Layers for a Multi-Team Design System
Naming looks like the trivial part of adopting layers, right up to the point where a reorganisation renames checkout-team and every stylesheet that wrote into it silently starts creating a brand-new layer at the top of the stack. Names in a layer manifest are a public API. This walkthrough turns the principles from layer naming conventions and governance into a concrete decision procedure, within the architecture patterns reference.
Prerequisites
You need to know that an unrecognised layer name is created implicitly at the end of the stack — the highest normal priority — with no warning from the browser. That single behaviour is why naming needs a process rather than a convention.
/* WHY: the manifest is the contract. Every name below is reviewed, owned and
linted; anything not in this list is a bug by definition. */
@layer reset, vendor, base, theme, components, utilities, overrides;Tooling: Stylelint with @csstools/stylelint-plugin-cascade-layers, and a CODEOWNERS file.
Step 1: List the cascade decisions before inventing names
Names come last. Start by writing the ordering rules the architecture has to guarantee:
/* Decisions this system must encode:
1. Browser corrections lose to everything we write.
2. Third-party CSS loses to our own components.
3. Design tokens lose to the components that consume them.
4. Utilities beat components (that is what makes them utilities).
5. A product team can patch a design-system bug without a fork.
Five decisions → five bands. The names follow from the decisions,
not the other way round. */What this does: produces a stack whose size is justified. Teams that skip this step reliably end up with eleven layers, four of which encode no decision at all.
Step 2: Name the top level by purpose, in lowercase kebab-case
/* GOOD — each name states what the band is FOR. Still accurate after any
reorganisation, acquisition or platform rewrite. */
@layer reset, vendor, base, theme, components, utilities, overrides;
/* BAD — names that encode transient facts.
@layer v2-styles; ← what happens at v3?
@layer platform-team; ← what happens when platform merges with core?
@layer new-design; ← "new" has a half-life of about nine months
@layer temp-fixes; ← nothing named temp has ever been removed */What this does: guarantees that a rename is never forced by an event outside the CSS. The test to apply: would this name still be correct if the company reorganised tomorrow?
Step 3: Give each team a sub-layer, not a band
/* WHY: teams own children of `components`, so their rules inherit the
band's position automatically. A team can be added, renamed or merged
without touching the top-level contract that everyone depends on. */
@layer components.core, /* design system */
components.checkout, /* checkout */
components.search, /* search */
components.account; /* identity *//* WHY: sibling order inside `components` still matters — a later sibling
beats an earlier one. Put `core` first so product teams can specialise
design-system components without escalating specificity. */
@layer components {
@layer core { .btn { padding: var(--space-3) var(--space-6); } }
@layer checkout { .btn { padding-inline: var(--space-8); } } /* wins */
}What this does: turns a team boundary into a one-line change and keeps the number of things a new engineer must learn at seven, not seventeen.
Step 4: Reserve the bands you do not need yet
/* WHY: `vendor` and `overrides` are declared before anything uses them.
Reserving costs nothing — an empty layer holds its slot — and it means
the day someone needs to wrap a third-party stylesheet, the position is
already reviewed and no urgent manifest change is required. */
@layer reset, vendor, base, theme, components, utilities, overrides;What this does: removes the most common source of an emergency reorder. The alternative — adding vendor under deadline pressure — is exactly when it gets appended in the wrong place.
Step 5: Publish and lint the names
{
"plugins": ["@csstools/stylelint-plugin-cascade-layers"],
"rules": {
"csstools/no-unknown-layers": true,
"csstools/cascade-layer-name-pattern": ["^[a-z][a-z0-9-]*$"]
}
}# Fails the build when a name outside the manifest appears anywhere.
npx stylelint "src/**/*.css" --max-warnings 0What this does: converts the convention into a check. Without it the naming scheme survives exactly as long as the person who wrote it stays on the team.
Verification
- Read the manifest aloud. Each name should be explainable in one sentence that mentions no team, version or date.
- Confirm every team’s rules land in a
components.*child, not in a new band: search for@layerstatements outside the manifest file. - Run the lint rule against a deliberately broken branch that writes
@layer nonsense { }and confirm CI fails. - Check the emitted bundle registers the layers in manifest order:
grep -m1 -o "@layer [^;]*;" dist/assets/index.cssTroubleshooting
A team’s rules stopped applying after a merge.
: Two teams’ sub-layers were reordered, and the later sibling now wins. Sibling order inside components is part of the contract; fix it in the manifest rather than by escalating specificity in the losing file.
A new layer name appeared in production and nobody added it.
: A dependency shipped its own @layer statement. Pin the version, mirror the name in your manifest at the position you want, and add the package to the upgrade checklist.
The lint rule fires on the manifest file itself.
: The manifest legitimately contains bare @layer statements and no rules. Add an override for that path rather than weakening the rule globally.
A name is correct but nobody uses it.
: Reserved bands are meant to be empty. Only remove one when you are confident the decision it encodes no longer exists — deleting vendor because it is unused is how it gets appended in the wrong place six months later.
Complete working example
/* ============================================================
layers.css — the manifest, imported first from the entry point
============================================================ */
/* WHY: seven bands, one per ordering decision, purpose-named so no
reorganisation can invalidate them. Owners are recorded inline because
this is the file people actually read during an incident. */
@layer reset, /* @org/design-system — browser corrections only */
vendor, /* @org/design-system — wrapped third-party CSS */
base, /* @org/design-system — element defaults, typography */
theme, /* @org/brand — tokens, colour schemes */
components, /* shared band, owned per team via sub-layers */
utilities, /* @org/design-system — atomic helpers */
overrides; /* product teams — time-boxed, expiry required */
/* WHY: sibling order inside `components` is declared explicitly so product
teams can specialise design-system components without !important, and so
the order is reviewable rather than emergent from import order. */
@layer components.core,
components.account,
components.search,
components.checkout;
/* WHY: vendor children are named per package, so dropping a dependency is
a one-line deletion and an audit can see exactly what is vendored. */
@layer vendor.normalize, vendor.tiptap, vendor.leaflet;
@import url("normalize.css") layer(vendor.normalize);
@import url("@tiptap/core/styles.css") layer(vendor.tiptap);
@import url("leaflet/dist/leaflet.css") layer(vendor.leaflet);
/* WHY: during the checkout→payments rename, BOTH names are declared,
adjacent so priority is identical. Remove `components.checkout` once the
lint rule reports no remaining consumers. Planned removal: 2026-10-01. */
@layer components.payments;Frequently Asked Questions
Should each team get its own top-level layer?
No. Every top-level layer is a fact each engineer has to hold in their head to predict a conflict, and team structures churn far faster than cascade decisions do. Giving teams nested children under components means their rules inherit a reviewed position automatically, and a merge or rename touches one line in the manifest instead of inverting priorities across the application.
How do you rename a cascade layer safely?
Declare both names for one release, migrate consumers with the lint rule warning on the old one, then delete it. The overlap window exists because an unrecognised layer name does not error — it is created implicitly at the top of the stack, so a consumer that missed the rename does not break loudly; it quietly acquires the highest priority in the application, which is much harder to notice.
Is there a naming convention for nested layers?
Lowercase kebab-case, one level of nesting beneath a top-level purpose layer, and a child name matching the owning team or package. components.checkout and vendor.leaflet both tell a reviewer the purpose and the owner at a glance. Anything deeper — components.checkout.form.field — duplicates structure the selectors already express, and makes the manifest harder to read than the CSS it governs.
Related
- Layer Naming Conventions and Governance — the contract this naming scheme serves
- Enforcing a Layer Contract With Stylelint Rules — the CI configuration in full
- Documenting Layer Architecture in a Monorepo — publishing the manifest so consumers can follow it
- Structuring @layer for Scalable Component Libraries — the component band these names organise
Up: Layer Naming & Governance → Architecture Patterns & Design System Scaling