Devie UI supports several themes in one application with several sets of CSS variables. The active set is selected by the data-devie-theme attribute on the <html> element.
Scope each set of variables to a data-devie-theme value instead of :root. Then set the attribute on the <html> element:
@import "./themes/default";
@import "./themes/dark";Change the attribute to change the theme, for example with document.documentElement.dataset.devieTheme. Wire it to any trigger in your app.
To keep the choice after a refresh, store it and restore it before the first paint. A prebuilt site cannot personalize the HTML per request. Store the theme in Local Storage and run a small inline script in layout.tsx before hydration.
The options and their trade-offs:
| Approach | Trade-offs |
|---|---|
| No persistence | The attribute is hardcoded on <html> and resets on refresh. No flash. |
| Local Storage after hydration | Simple, but the theme is restored after React runs. The default theme can flash first. |
| Local Storage + early boot script | Recommended for static exports. Pages stay prebuilt, and the attribute is restored before hydration. |
| Cookies + SSR | Use it when the server must know the theme before it sends HTML. It prevents a static export. |
| DB + SSR | Same as cookies, with the preference read from your backend. Fits authenticated apps that already render on the server. |
The layout injects a boot script. The ThemeContext keeps the attribute and Local Storage in sync after mount.
import { ThemeProvider } from "@/ui/themes/ThemeContext";
const THEME_STORAGE_KEY = "devie-theme";
const DEFAULT_THEME = "theme-default";
const ALLOWED_THEMES = ["theme-default", "theme-dark"];
const themeBootScript = `(function () {
try {
var allowedThemes = new Set(${JSON.stringify(ALLOWED_THEMES)});
var storedTheme = localStorage.getItem("devie-theme");
document.documentElement.dataset.devieTheme =
allowedThemes.has(storedTheme) ? storedTheme : "theme-default";
} catch {
document.documentElement.dataset.devieTheme = "theme-default";
}
})();`;
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" data-devie-theme={DEFAULT_THEME} suppressHydrationWarning>
<head>
<script id="theme-boot">{themeBootScript}</script>
</head>
<body>
<ThemeProvider defaultTheme={DEFAULT_THEME}>
{children}
</ThemeProvider>
</body>
</html>
);
}Use SSR when the server must know the theme before it renders. For a data attribute on <html> alone, it is overkill.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultTheme | string | "theme-default" | The initial theme class name |
| Property | Type | Description |
|---|---|---|
selectedTheme | string | The currently active theme |
setTheme(theme) | (string) => void | Set and persist a new theme |
previewTheme(theme) | (string) => void | Preview a theme without persisting |
clearPreviewTheme() | () => void | Clear the preview, revert to selected |
primaryColor | string | Computed primary color from current theme |