Use CSS @layer to control cascade precedence across design system, third-party, and product CSS — eliminating specificity wars and making overrides predictable without !important.
Use CSS @layer to control cascade precedence across design system, third-party, and product CSS — eliminating specificity wars and making overrides predictable without !important.
CSS @layer creates named cascade layers where rules in later layers override earlier layers regardless of specificity. This is a paradigm shift: in unlayered CSS, a class selector (specificity 0,1,0) loses to an ID selector (1,0,0). In layered CSS, a rule in a higher-priority layer wins regardless of the selector used.
1/* Declare layer order — last wins */
2@layer reset, tokens, base, components, utilities, overrides;
3
4@layer reset {
5 *, *::before, *::after { box-sizing: border-box; }
6 body { margin: 0; }
7}
8
9@layer tokens {
10 :root {
11 --color-interactive-primary: #3B82F6;
12 --radius-component: 6px;
13 }
14}
15
16@layer components {
17 /* Design system Button — specificity does not matter here */
18 .button { background: var(--color-interactive-primary); }
19}
20
21@layer overrides {
22 /* Product override — wins regardless of specificity */
23 .button { background: hotpink; }
24}The layer order declaration at the top locks in the cascade hierarchy for the entire stylesheet. Styles in overrides always win over styles in components, even if the components layer uses more specific selectors.
A well-structured design system uses layers for each CSS tier:
1/* packages/tokens/dist/tokens.css */
2@layer ds.reset, ds.tokens, ds.base;
3
4@layer ds.reset {
5 /* Normalize browser defaults */
6}
7
8@layer ds.tokens {
9 :root {
10 --color-interactive-primary: #3B82F6;
11 /* All semantic tokens */
12 }
13}
14
15@layer ds.base {
16 /* Typography scales, focus styles */
17 :focus-visible {
18 outline: 3px solid var(--color-interactive-primary);
19 outline-offset: 2px;
20 }
21}1/* packages/react/dist/components.css */
2@layer ds.components;
3
4@layer ds.components {
5 .ds-button {
6 background: var(--color-interactive-primary);
7 border-radius: var(--radius-component);
8 padding: var(--space-component-padding);
9 }
10
11 .ds-dialog {
12 background: var(--color-surface-raised);
13 border-radius: var(--radius-dialog);
14 }
15}1/* In consuming app: product overrides */
2@layer ds.reset, ds.tokens, ds.base, ds.components, product;
3
4@layer product {
5 /* This always wins over ds.components — no !important needed */
6 .ds-button { border-radius: 0; } /* Square buttons for this product */
7}Third-party CSS libraries (Bootstrap, Tailwind's preflight, third-party component CSS) should be wrapped in a low-priority layer to prevent conflicts with your design system:
1/* Wrap any third-party CSS in a named layer */
2@layer third-party;
3
4@import url('https://cdn.example.com/some-library.css') layer(third-party);
5
6/* Or in your build pipeline: */
7@layer third-party {
8 @import './vendor/bootstrap-grid.css';
9}Tailwind CSS v4 uses @layer automatically with its preflight and utilities layers. If you are combining a design system with Tailwind, declare explicit layer order to ensure Tailwind utilities do not override design system components:
1@layer ds.reset, ds.tokens, ds.base, ds.components, tailwind.utilities, product;
2
3@import '@acme/tokens/dist/tokens.css';
4@import '@acme/react/dist/components.css';
5@import 'tailwindcss/utilities' layer(tailwind.utilities);Tailwind utilities win over design system components (intentional — utility classes are meant to override), but product layer wins over both.
Ensure @layer declarations survive build pipelines without being reordered or duplicated:
1// postcss.config.mjs
2import postcssLayerPolyfill from '@csstools/postcss-cascade-layers';
3
4export default {
5 plugins: [
6 // Polyfill for browsers that do not support @layer (none in 2026 with >2% usage)
7 // Keep for Safari 14.1 compatibility if needed:
8 postcssLayerPolyfill(),
9 ],
10};1// vite.config.ts for design system build
2export default defineConfig({
3 css: {
4 // Prevent Vite from merging @layer declarations across files
5 // which can break layer order
6 preprocessorOptions: {},
7 },
8 build: {
9 rollupOptions: {
10 output: {
11 // Keep CSS files separate rather than merging into one bundle
12 // so layer order is determined by import order in consuming apps
13 assetFileNames: (assetInfo) =>
14 assetInfo.name?.endsWith('.css')
15 ? 'styles/[name][extname]'
16 : '[name][extname]',
17 },
18 },
19 },
20});Critical: if your bundler merges all CSS into a single file, ensure the @layer order declaration appears first in the merged output. Test this by inspecting the built CSS and searching for @layer declarations out of order.
CSS @layer can confuse teams unfamiliar with it. Build a debugging guide:
1# CSS Layer Debugging Guide
2
3## Why is my override not working?
4
51. Open DevTools > Elements > Computed styles
62. Find the property you are trying to override
73. Look at the cascade origin — if it shows `ds.components` and your
8 override is in `ds.tokens` or `ds.base`, you are in a lower-priority layer.
9
10## Fix options:
11
12### Option A: Move your override to the `product` layer (recommended)@layer product {
.ds-button { background: var(--color-brand-special); }
}
1
2### Option B: Use a CSS custom property (no layer needed)/* Override the component token directly */
.special-section .ds-button {
--button-background: var(--color-brand-special);
}
1
2### Option C: Never do this/* !important bypasses layers entirely — creates a new mess */
.ds-button { background: hotpink !important; }
1
2## Layer order in this project
31. ds.reset (lowest)
42. ds.tokens
53. ds.base
64. ds.components
75. tailwind.utilities
86. product (highest — your overrides go here)Publish this in your design system docs under 'Troubleshooting > CSS Cascade'. Link it from error messages when teams report 'my styles are not applying'.
Add @layer declarations to your design system's main CSS file. Start with three layers (tokens, components, utilities) and declare them in order. Test that a product-layer override correctly beats a component-layer rule without !important.
Your progress is saved in this browser, on this device only.