Theme Engine

Runtime theming system: CSS variable override, scoped themes, localStorage persistence, and cascade strategy.

Runtime Token Override

CSS custom properties cascade like any other inherited value. Override any token at runtime by writing directly to element.style.setProperty() — no build step, no class toggling. Changes propagate instantly to every component inside the scope.

Component Preview Live

All colors, radius, and surface tokens below are injected as CSS custom properties onto this container. Any nested component inherits them automatically.

Second Card

Same tokens, different content — demonstrating consistent cascade across sibling elements inside the same scope.

Active token values

Scoped Themes

Apply a data-theme attribute to any element to scope a DaisyUI theme to just that subtree. Both panes below exist in the same DOM — no separate stylesheet, no JavaScript toggle.

data-theme="light"

Card component

Uses DaisyUI's base-100, base-content semantic tokens — resolves to the light palette here.

Primary
Neutral

data-theme="dark"

Card component

Same markup, same classes — resolves to the dark palette because the ancestor declares data-theme="dark".

Primary
Neutral

Mix custom themes the same way — data-theme="brand-warm" on a section, data-theme="high-contrast" on a modal. DaisyUI resolves each element to the nearest ancestor theme scope.

localStorage Persistence

Persist the active theme across sessions using localStorage. The pattern below applies the theme before the first paint to avoid flash.

Active theme:

Saved to localStorage — persists on reload.

<!-- In <head>, before any stylesheet -->
<script>
  (function() {
    var t = localStorage.getItem('theme') || 'light';
    document.documentElement.setAttribute('data-theme', t);
  })();
</script>

The IIFE runs synchronously before DaisyUI's stylesheet is parsed — no flash of incorrect theme on first paint.

Cascade Strategy

When multiple scopes define the same token, the nearest ancestor wins. Understand the cascade order to reason about where a resolved value comes from.

:root { --color-primary: … } Global default — token.css → lowest
[data-theme="brand"] { --p: … } DaisyUI preset on body or html
.section { --color-primary: … } Section-scoped override
[data-theme="dark"] { … } Nested scoped theme
el.style.setProperty('--color-primary', …) Inline style — always wins → highest ✓

Inline style has the highest specificity for custom properties. Use it for one-off runtime overrides. Prefer data-theme scopes for reusable, named contexts — they stay in CSS and don't mix styling concerns into JavaScript.

SSR-Safe Hydration

Server-rendered pages can echo the correct data-theme from a session/cookie so the HTML arrives already themed — Alpine.js then syncs its own state from the DOM on mount, not the other way around.

<?php $theme = $_COOKIE['theme'] ?? 'light'; ?>
<html data-theme="<?= htmlspecialchars($theme) ?>">
// Alpine reads from DOM, never overwrites on init
x-data="{ theme: document.documentElement.dataset.theme }"
x-init="$watch('theme', t => {
  document.documentElement.dataset.theme = t;
  document.cookie = 'theme=' + t + ';path=/';
})"
1

Server echoes theme

PHP reads cookie/session and sets data-theme on before any JS runs.

2

CSS resolves instantly

DaisyUI reads data-theme during stylesheet parsing — zero JS required for initial paint.

3

Alpine syncs on mount

x-init reads the existing DOM value; watcher fires only when the user changes theme.