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: unlayered styles always 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. Highest priority — always wins.

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-specifiy a component selector or reach for !important.

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, always wins -->
<link rel="stylesheet" href="my-overrides.css">

Using with Tailwind CSS

Tailwind v4 uses its own @layer declarations. If you're mixing shadcn-html components with Tailwind utilities, 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 Tailwind 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 always wins 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 Tailwind, other libraries, or your own @layer declarations. Future-proof @layer is a W3C standard, built into every modern browser. Not a framework convention.

Why not @scope or Web Components?

Every layer of abstraction is a layer AI can get wrong. Web Components add class definitions, lifecycle callbacks, Shadow DOM, and template cloning. @scope strips context from class names. Both make the system harder for AI to read, generate, and modify — 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. Flat, visible DOM Shadow DOM hides structure inside custom elements. An AI can't inspect, modify, or learn from what it can't see. Plain HTML gives full visibility into every component. Self-documenting class names A class like .card-header carries its meaning in the name — an AI can generate it without knowing the surrounding markup. With @scope, a generic .header is ambiguous. Context is everything for AI generation. 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 AI produces correct output.

Simplicity isn't a limitation — it's a deliberate design choice that maximizes the context available to AI. Every pattern in this system was chosen not just to avoid complexity, but to actively give AI more to work with.

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.