Layer Naming Conventions and Governance
Cascade layers fail in large organisations for a social reason, not a technical one: the order is a shared contract, and nothing stops an engineer in one repository from adding @layer checkout-fixes at the bottom of a file and quietly acquiring the highest priority in the entire application. Layer order is only as reliable as the process that guards it. This guide sits under architecture patterns and design system scaling and covers the naming schemes, ownership model and lint rules that turn a convention into an enforced contract.
Concept and spec reference
Two behaviours in CSS Cascade Level 5 make governance necessary rather than optional:
- Layers are created implicitly on first use. Writing
@layer anything { … }with no prior declaration creates that layer at the end of the current stack — the highest normal priority. There is no error, no warning, and no way to detect it at runtime. - First declaration wins the position. Once a layer name has been registered, later
@layerstatements naming it do not move it. Order is decided by first appearance, which means whichever file loads first defines the contract for everyone.
Together those rules mean the only safe place for order to be decided is a single file that is guaranteed to load first.
/* layers.css — the manifest. This file is imported before anything else and
is the ONLY file permitted to contain a bare @layer statement.
WHY: first declaration fixes each layer's position for the whole
application, so centralising it makes the order reviewable in one diff. */
@layer reset, /* owner: design-system — UA corrections only */
vendor, /* owner: design-system — third-party CSS, imported wrapped */
base, /* owner: design-system — element defaults and typography */
theme, /* owner: brand — tokens, colour schemes, density */
components, /* owner: design-system + product teams via sub-layers */
utilities, /* owner: design-system — atomic helpers */
overrides; /* owner: product teams — time-boxed escape hatch */How the contract is actually enforced at parse time
The browser gives you no enforcement, so it is worth being precise about what it does guarantee, and therefore what tooling has to cover.
- The manifest loads first. Its comma list registers every name in order. Positions are now fixed for the document.
- A later file writes
@layer components.checkout { … }. The parentcomponentsalready exists, so the child is created at the end ofcomponents’ sub-list. Priority is inherited from the parent’s position — safe. - A later file writes
@layer checkout-fixes { … }. The name is unknown. The browser creates it at the end of the top-level stack, aboveoverrides. This rule now beats every design-system declaration in the application. Nothing in the browser reports this. - The manifest is edited to add
checkout-fixesin the middle. Position is taken from the manifest because it is parsed first. The stack is correct again — which is exactly why the fix for step 3 is always “add it to the manifest”, never “reorder the file that uses it”.
Step 3 is the failure mode governance exists to prevent, and because it is invisible at runtime it has to be caught statically. That makes the lint rule non-negotiable rather than a nicety.
Practical naming patterns
Pattern 1 — purpose at the top, ownership below
Name top-level layers for what they do, and put team identity in the nested segment.
/* GOOD: purpose-named top level, owner-named children.
WHY: reorganisations rename branches; the cascade contract is untouched. */
@layer components.core, components.checkout, components.search;
/* BAD: team names in the top-level stack.
WHY: when checkout merges into payments, every consumer's cascade order
changes, and the rename is a breaking change for anyone importing it. */
@layer checkout-team, search-team;Pattern 2 — the reserved vendor band
Third-party CSS needs a dedicated band low in the stack so it can never outrank your components, and a naming rule that identifies the package.
/* WHY: one child per package makes it obvious at a glance what is vendored,
and lets you drop a package's styles by deleting a single import line. */
@layer vendor.normalize, vendor.leaflet, vendor.tiptap;
@import url("normalize.css") layer(vendor.normalize);
@import url("leaflet/dist/leaflet.css") layer(vendor.leaflet);
@import url("tiptap/styles.css") layer(vendor.tiptap);This is the naming half of resolving third-party CSS conflicts; the mechanics of wrapping an import live there.
Pattern 3 — the expiring overrides layer
An overrides layer is a pressure valve. Without an expiry policy it silently becomes permanent.
@layer overrides {
/* OWNER: checkout · EXPIRES: 2026-09-30 · TICKET: DS-4412
WHY: design system ships the wrong disabled-state contrast for
.btn--ghost. Delete this block when DS-4412 lands. */
.checkout-panel .btn--ghost:disabled { color: var(--color-text-muted); }
}Pair that comment convention with a CI job that fails once the date passes, and the layer stays small. The auditing side is covered in auditing the overrides layer in CI.
Interaction with adjacent features
Build pipelines. The manifest must be emitted first in the bundled output, which is a build-order concern rather than a source-order one — see build-pipeline layer automation for enforcing that in Vite and PostCSS.
Micro-frontends. Independently deployed applications each parse their own CSS, so a shared manifest has to be published as a package and imported by every shell. Micro-frontend cascade isolation covers the coordination problem when no single document owns the order.
Nested sub-layers. Sub-layer order is fixed by first appearance exactly like the top level, so each team’s child list needs the same pre-declaration discipline — see nested layers and inheritance.
Component isolation. Naming is what makes component layer isolation reviewable: a path like components.search.results tells a reviewer both the owner and the blast radius without opening another file.
Stylelint enforcement workflow
- Install the plugin and point it at the manifest:
npm install --save-dev stylelint @csstools/stylelint-plugin-cascade-layers- Configure the two rules that matter — no unknown layers, and nothing outside a layer:
{
"plugins": ["@csstools/stylelint-plugin-cascade-layers"],
"rules": {
"csstools/no-unknown-layers": true,
"csstools/no-unlayered-rules": true,
"csstools/cascade-layer-name-pattern": ["^[a-z][a-z0-9-]*$"]
},
"overrides": [
{
"files": ["src/styles/layers.css"],
"rules": { "csstools/no-unlayered-rules": null }
}
]
}- Run it in CI so an implicitly created layer fails the build rather than shipping:
npx stylelint "src/**/*.css" --max-warnings 0- Gate the manifest itself in review with a code-owners entry, so changing the cascade order requires the design system team’s approval:
# .github/CODEOWNERS
src/styles/layers.css @org/design-system- Publish the rendered layer map alongside the component documentation. A contract nobody can see is a contract nobody follows.
Adoption checklist
- Create the manifest file and move every existing
@layerstatement into it. - Import it first from the application entry point, before any component or vendor CSS.
- Record an owner per layer as an inline comment; the comment is the documentation most people will actually read.
- Convert ad hoc layers. For each layer name found in the codebase but missing from the manifest, decide its correct position and add it — do not simply append.
- Turn on the lint rules in warning mode, fix the reported files, then switch to error.
- Add the code-owners rule for the manifest path.
- Document the expiry convention for the overrides layer and add the CI check that enforces it.
- Re-render the layer map whenever the manifest changes, and link it from the design system home page.
Edge cases and gotchas
A layer named in the manifest but never used still holds its slot
That is a feature, not waste. Reserving vendor before you have any vendored CSS means the day someone adds it, the position is already correct and no manifest change is needed in a hurry.
Import order can defeat the manifest
If a bundler hoists a component stylesheet above the manifest — a common result of CSS-in-JS injection or an @import inside a component file — the component’s layer names register first and the manifest becomes decorative. Verify the emitted bundle, not the source order.
Renaming a layer is a breaking change
Every consumer that writes into components.checkout breaks silently when it becomes components.payments: their rules land in a newly created top-level layer. Ship renames with a deprecation window where both names are declared, and lint for the old name.
Deep nesting outlives its usefulness fast
components.checkout.form.field.error is legal and almost always a mistake — the depth encodes information that selectors already carry. Two levels below a top-level layer is a practical ceiling.
The manifest cannot fix what a dependency already registered
A design-system package that ships its own @layer statement inside its distributed CSS registers those names the moment the file is parsed. If that happens before your manifest — because the package is imported at the top of a component file rather than through your entry point — the dependency has defined part of your cascade order and your manifest inherits it. The fix is a contract with the package rather than a code change: dependencies should either declare no layers at all and let consumers wrap them with @import … layer(), or document the exact names and order they register so consumers can mirror them in their own manifest. When neither is true, pin the version and re-verify the emitted bundle on every upgrade, because a minor release can silently insert a new layer above yours.
Comment-only ownership decays
Owner comments beside the manifest entries are read constantly during incidents and never during routine work, which is why they drift. Anchor them to something that fails: a CODEOWNERS entry gives ownership teeth in review, and a short generated table in the design-system documentation gives it visibility. The comment then becomes a convenience rather than the source of truth, and a stale owner name is caught the first time a review request routes to a team that no longer exists.
Sub-layers do not create isolation
A rule in components.search can still match elements anywhere in the document. Layers order declarations; they do not scope selectors. Pair naming with @scope, Shadow DOM or a class-prefix convention when genuine encapsulation is the requirement — the trade-offs are compared in cascade layers vs CSS Modules vs Shadow DOM.
FAQ
How many top-level cascade layers should a design system have?
Five to seven is enough for almost any production system: reset, vendor, base, theme, components, utilities and optionally overrides. Every additional top-level layer is one more thing every engineer must hold in their head to predict a conflict. Depth beyond that should live in nested sub-layers, where only the owning team needs to reason about the order.
Should layer names describe purpose or ownership?
Purpose at the top level, ownership underneath. Top-level order encodes a cascade decision that should outlive any particular team structure, so naming it after a team guarantees a painful rename later. components.checkout gives you both facts — the purpose from the parent, the owner from the child — and survives reorganisation with a single-line change.
How do you stop teams from adding layers ad hoc?
Enforce it statically, because the browser will not. Keep every permitted name in one manifest file and enable csstools/no-unknown-layers in CI. Without that rule an unrecognised layer name is created implicitly at the top of the stack, where it outranks the entire design system, and no runtime signal ever tells you it happened.
What belongs in an overrides layer?
Time-boxed corrections only — each with an owner, a ticket and an expiry date recorded in a comment above the block. The moment entries start living there indefinitely, the layer has become a slower-moving version of !important: real design-system defects stay hidden behind local patches, and the override stack grows faster than anyone can audit it.
Can a consuming application add its own layers on top of ours?
It can and usually should — an application has ordering decisions a shared design system cannot anticipate, such as where a marketing landing page or an embedded partner widget belongs. Publish the manifest as an importable file and document that consumers may append names after overrides, never insert between existing ones. Appending is safe because it only adds priority above the contract; inserting silently changes the relationship between two bands other teams already depend on.
Guides in This Section
- Documenting Layer Architecture in a Monorepo — Publish a cascade layer contract package consumers can actually follow.
- Enforcing a Layer Contract With Stylelint Rules — Configure Stylelint so an undeclared cascade layer, an unlayered rule or a stray !important fails CI — rule by rule.
- Naming Cascade Layers for a Multi-Team Design System — A concrete naming decision walkthrough.
Related
- Override Layer Best Practices — how to keep the escape-hatch layer small and reviewable
- Component Layer Isolation — the sub-layer structure a naming scheme has to describe
- Build-Pipeline Layer Automation — guaranteeing the manifest is emitted before any other CSS
- Naming Cascade Layers for a Multi-Team Design System — the concrete naming decision walkthrough
- Enforcing a Layer Contract With Stylelint Rules — the full CI configuration, rule by rule