What is @layer?

CSS Cascade Layers are a native browser feature that lets stylesheets declare which bucket their rules belong to. When two selectors have the same specificity, the browser uses layer order to decide which one wins. The key insight: for normal rules, unlayered styles beat layered styles, regardless of selector specificity.

the rule in one sentence
/* Layered — from a component stylesheet */
@layer components {
  .btn { background: var(--primary); }
}

/* Unlayered — your CSS */
.btn { background: tomato; }

/* Result: tomato wins. Always. No !important needed. */

How shadcn-html uses it

Every component CSS file — button, card, dialog, all of them — wraps all of its rules inside a single layer declaration:

components/button/button.css
@layer components {
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--primary);
    color: var(--primary-foreground);
    border-radius: var(--radius-md);
    /* ... all button rules ... */
  }

  .btn[data-variant="outline"] {
    background: transparent;
    border: 1px solid var(--border);
  }
}
components/card/card.css
@layer components {
  .card {
    background-color: var(--card);
    color: var(--card-foreground);
    border: 1px solid var(--border);
    border-radius: var(--radius-xl);
    /* ... all card rules ... */
  }

  .card-header {
    padding: 1.5rem;
  }
}

The design tokens file (default-semantic-tokens.css) is intentionally not layered — token values sit at the top of the cascade so they're always available to both component and user styles.

The cascade order

Here's what the browser sees, from lowest to highest priority:

1
@layer components All component CSS — button, card, dialog, etc. Lowest priority.
2
default-semantic-tokens.css Design tokens (:root custom properties). Unlayered — always available.
3
Your styles Any unlayered CSS you write. Higher priority than the component layer.

Because component rules live inside @layer components and your CSS is unlayered, the browser gives your styles higher cascade priority automatically. You never need to out-specify a component selector or reach for !important just to beat a default component rule.

Note: Tokens and your styles are both unlayered — between them, normal specificity and source order apply. Tokens don't compete with your component overrides because they only define :root custom properties, not component selectors.

Overriding component styles

Write your overrides in a regular, unlayered stylesheet. They'll always take precedence.

components/button/button.css (layered)
@layer components {
  .btn {
    border-radius: var(--radius-md);
    font-weight: 500;
    height: 2.25rem;
  }
}
my-overrides.css (unlayered — wins)
/* Make buttons fully rounded */
.btn {
  border-radius: 9999px;
}

/* Make outline buttons thicker */
.btn[data-variant="outline"] {
  border-width: 2px;
}

Include your override file after the component stylesheets:

HTML — load order
<link rel="stylesheet" href="theme/default-semantic-tokens.css">
<link rel="stylesheet" href="components/button/button.css">
<link rel="stylesheet" href="components/card/card.css">

<!-- Your overrides — unlayered, beats the component layer -->
<link rel="stylesheet" href="my-overrides.css">

Using with other CSS frameworks

If you're mixing shadcn-html components with another CSS framework that uses its own @layer declarations, you can declare an explicit layer order at the top of your main stylesheet to control priority:

main.css — explicit layer order
/* Declare layer order: first listed = lowest priority */
@layer components, utilities;

/* Now framework utilities beat shadcn-html component styles,
   and your unlayered CSS beats everything. */

Without an explicit order declaration, layers are ordered by first appearance in the stylesheet. The explicit declaration gives you full control regardless of import order.

Why not just write unlayered CSS?

Traditional CSS libraries (Bootstrap, Bulma) ship unlayered styles. That creates problems when you try to customize them.

Without @layer
/* Library: high specificity */
.btn.btn-primary { background: blue; }

/* Your override: loses */
.btn { background: tomato; }  /* ✗ */

/* Forced to escalate */
.btn { background: tomato !important; }
With @layer (shadcn-html)
/* Library: layered */
@layer components {
  .btn { background: var(--primary); }
}

/* Your override: unlayered */
.btn { background: tomato; }  /* ✓ wins */

/* No !important needed. */
Clean overrides Your CSS beats layered component defaults without !important or specificity hacks. Predictable cascade Component internals can use any selector complexity — it won't leak into your layer. Composable layers Works cleanly alongside other libraries or your own @layer declarations. Future-proof @layer is a W3C standard, built into every modern browser. Not a framework convention.

A note on !important: Cascade layers reverse priority for !important rules — an !important declaration inside @layer components actually beats an unlayered !important. This system never uses !important, but be aware of this if you're mixing in third-party CSS that does.

Why not @scope or Web Components?

Shadow DOM creates a styling boundary that makes theming painful — external CSS can't reach inside, so every component needs its own token-plumbing or ::part() surface. SSR is complex, framework interop suffers, and devtools show an extra layer of indirection. @scope strips context from class names, making selectors ambiguous outside their block. Both add abstraction that makes the system harder to read, generate, and modify — for humans and AI alike — so we use neither.

Plain HTML is the simplest target A <button class="btn" data-variant="outline"> is just HTML. No custom element registration, no shadow root, no template cloning. Nothing to misunderstand, nothing to get wrong. Theming without boundaries Shadow DOM blocks external stylesheets from reaching component internals. With plain HTML, your CSS custom properties and overrides apply everywhere — no ::part() or adopted stylesheets required. Flat, visible DOM Shadow DOM hides structure behind custom elements and adds another inspection boundary. DevTools can still reveal open shadow roots, but plain HTML keeps every component directly visible in the main DOM tree and easier for tools to reason about. Self-documenting class names A class like .card-header carries its meaning in the name — it can be generated without knowing the surrounding markup. With @scope, a generic .header is ambiguous outside its block. The cascade delivers context for free .card-title inherits font and color tokens from its parent .card automatically. This inheritance is the context — the more of it embedded in the structure, the more reliably tools and AI produce correct output.

Simplicity isn't a limitation — it's a deliberate design choice. Every pattern in this system was chosen not just to avoid complexity, but to maximize theming flexibility, tooling compatibility, and the context available to AI.

Browser support

@layer is supported in all modern browsers. No polyfill needed.

browser support
BrowserSupported since
Chrome / Edge99 (March 2022)
Firefox97 (February 2022)
Safari15.4 (March 2022)

See caniuse.com/css-cascade-layers and the MDN @layer reference for full details.