@theme inline

[check: …]
📖 Pair this live Tailwind demo with the companion guide (.md) — this page is the rendered ground truth. ↗ Builds on oklch_colors (static @theme color tokens) and frontend/tailwind: design tokens (the @theme namespace) — @theme inline is the dynamic-resolution variant.

regular @theme → :root · @theme inline → element

A regular @theme block emits CSS variables to :root, and utilities reference them with var(). The catch: if your theme value itself contains a var() — like --color-x: var(--brand) — that inner reference is resolved ONCE at :root computed-value time, then the result is inherited down the tree. Change --brand on a child and the utility does not follow. @theme inline flips this: it skips emitting the :root var and inlines the value into each utility, so var(--brand) resolves at the element that actually uses the class — picking up whatever --brand is in scope there. Dynamic theming works.

aspect@theme@theme inline
emits --color-x to :root? ✅ yes — utility is background: var(--color-x) ❌ no — value is inlined into the utility directly
generated utility .bg-x { background-color: var(--color-x) } .bg-x { background-color: var(--brand) } (value inlined)
where the inner var() resolves at :root (computed once, result inherited) at the element using the utility (live, per-scope)
best for static values — oklch colors, fixed spacing, font stacks values that reference OTHER css vars — dynamic themes, JS control
does descendant --brand override propagate? ❌ no — frozen to :root's value ✅ yes — utility reads the element's scope

1 · toggle --brand on the container — watch which swatch moves

Both swatches below are inside the same demo container. The container's --brand starts as the inherited :root default (slate gray). Click Set --brand = cyan to override --brand on the container only. The bg-dynamic swatch follows (resolves var(--brand) at the element). The bg-broken swatch does not — its value was frozen at :root.

bg-broken · @theme

compiled: background: var(--color-broken)
--color-broken was resolved at :root → frozen.

bg-dynamic · @theme inline

compiled: background: var(--brand)
var(--brand) resolves at this element → live.

--brand(container): — · bg-broken: — · bg-dynamic: —

2 · why regular @theme freezes (the cascade mechanism)

CSS custom properties are inherited as computed values. When --color-broken: var(--brand) is declared on :root, the browser computes --color-broken by substituting var(--brand) using the value of --brand at :root. The substituted result is then inherited. A descendant that overrides --brand is too late — its inherited --color-broken is already the frozen :root-resolved value.

/* regular @theme — :root emission */ :root { --brand: oklch(0.62 0.02 264); /* default slate */ --color-broken: var(--brand); /* ← resolved HERE at :root */ } /* → --color-broken = slate, inherited */ .demo-container { --brand: oklch(0.7 0.15 195); } /* override on child */ .bg-broken { background-color: var(--color-broken); } /* still slate ❌ */ /* ─────────────────────────────────────────────────── */ /* @theme inline — value inlined, no :root var emitted */ @theme inline { --color-dynamic: var(--brand); /* ← NOT resolved at :root */ } /* the var() is inlined into utilities */ .bg-dynamic { background-color: var(--brand); } /* resolves at element → cyan ✅ */
step@theme (broken)@theme inline (dynamic)
1. define token --color-broken: var(--brand) → written to :root value var(--brand) marked for inlining; no :root var
2. compile utility .bg-broken { bg: var(--color-broken) } .bg-dynamic { bg: var(--brand) } ← inlined
3. compute at :root --color-broken resolves var(--brand) → slate; inherited as slate nothing to compute at :root — no var emitted
4. element uses utility reads inherited (frozen) --color-broken → slate reads --brand in its own scope → cyan
5. descendant overrides --brand ❌ ignored — inherited value already settled ✅ honored — resolution is at the element

3 · when to use which · css var chaining

Rule of thumb: if the value is a literal (oklch, hex, px, a font stack), use @theme. If the value references another css variable — especially one that changes per data-theme, per component, or via JS — use @theme inline.

scenariousewhy
brand color = literal oklch value @theme static — emit to :root, cache forever
brand color = var(--user-brand) set by JS at runtime @theme inline must resolve at element so JS updates are picked up
[data-theme="dark"] swaps --bg, utilities follow @theme inline per-scope resolution — dark mode overrides cascade into utilities
spacing scale = fixed rem values @theme static — never changes
component-local --accent bridged into bg-accent @theme inline each component sets its own --accent; utilities follow locally
aliasing one token to another (--color-link: var(--color-brand)) @theme inline so redefining --color-brand later propagates to --color-link

CSS var chaining: with @theme inline, a chain like --color-link → var(--color-brand) → var(--brand) resolves link-by-link at the element, so the leaf (--brand) is live. With regular @theme, the whole chain collapses at :root on first compute — the leaf is baked in and later overrides are invisible.

/* Chaining with @theme inline — leaf is live */ :root { --brand: oklch(0.7 0.15 250); } /* leaf */ @theme inline { --color-brand: var(--brand); /* link 1 — inlined */ --color-link: var(--color-brand); /* link 2 — inlined (NOT --color-link) */ } /* .text-link → color: var(--color-brand) → (element scope) → var(--brand) → live */ [data-theme="night"] { --brand: oklch(0.4 0.12 250); } /* leaf override */ /* text-link inside [data-theme="night"] now reads the night brand automatically. */

intent → pattern

intentpatternwhy
static brand color (literal) @theme { --color-brand: oklch(0.7 0.15 250); } emitted to :root; bg-brand = var(--color-brand)
bridge an external css var @theme inline { --color-brand: var(--user-brand); } bg-brand inlines to var(--user-brand) — resolves at element
JS-controlled theme JS sets el.style.setProperty('--user-brand', cyan) with @theme inline, bg-brand on descendants updates live
data-theme palettes [data-theme="dark"] { --bg: #000 } + @theme inline { --color-bg: var(--bg) } utilities follow whichever theme scope the element is in
alias one token to another @theme inline { --color-link: var(--color-brand); } keeps the alias live — editing the source token propagates
check resolution scope at runtime getComputedStyle(el).backgroundColor if it changes when you set --brand on a child → inline worked