shadcn-html / cascade-layers
Cascade Layers
Every component stylesheet wraps its rules in @layer components.
This is a single architectural decision that gives you guaranteed override power —
your own CSS always wins over component defaults, without !important.
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.
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:
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:
@layer components
All component CSS — button, card, dialog, etc. Lowest priority.
default-semantic-tokens.css
Design tokens (:root custom properties). Unlayered — always available.
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.
Include your override file after the component stylesheets:
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:
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.
!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.
<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.
See caniuse.com/css-cascade-layers and the MDN @layer reference for full details.