Devie UI
Version: 2026-02-14

Manage More Themes

In Devie UI, supporting multiple themes in one application can be achieved relatively simply by having multiple available sets of CSS Variables to swap between.

Our proposed solution relies on classes to scope the CSS variables: the app knows what set of variables is effective based on the class applied on the root <html> element.

Define Themes

The main idea here is to scope the definition of variables to a className instead of applying them to the root.

Then, apply the className corresponding to your active theme on the html element:

@import "./themes/default";
@import "./themes/dark";

Set the Active Theme

You can now simply change the class to change theme. This can be done using document.documentElement.className for example, and can be linked to the business logic of your choice as to what action triggers the theme change.

Persist Theme on Refresh

If you want a user's theme choice to survive a page refresh, you will need to store it somewhere and restore it early enough to avoid a flash of the default theme.

The tricky part is timing. Client-side storage (like Local Storage) isn't available until JavaScript hydrates, so the page may briefly render with the default theme before the saved preference is applied.

You have multiple options:

ApproachTrade-offs
No persistenceIf you don't need to store the theme, the default class can be hardcoded on the <html> and resets on refresh. You won't have any flash.
Store in Local StorageLocal Storage is only available after JavaScript hydrates, which means your page will first load once before your client code can fetch the theme and apply it, causing the UI to flash. This approach is simple but only makes sense if you don't mind the performance hit of delaying the render of your page after JS loads (e.g. for SPA behind a login).
Store in Cookies + SSRThe theme is stored in the user's browser cookies, and set alongside the request to the server. The server reads the cookie before rendering, so the correct theme is applied from the first byte. No flash of the wrong theme. This approach requires server-side rendering (i.e. won't work with static exports).
Store in DB + SSRIf your users are authenticated, you can also read the value in the Database to know which class name to apply. This is relevant for server-side logics where the backend fetches the user information and builds the page to render before sending it to the client.

Example: Implementing a ThemeContext

With Local Storage

Here's a proposed implementation of the theme manager that stores the theme in local storage. The theme is automatically loaded once the client-side code runs.

import { ThemeProviderWithLocalStorage } from "@/ui/themes/ThemeContextWithLocalStorage";
 
function App() {
  return (
    <ThemeProviderWithLocalStorage autoLoadTheme={true}>
      {/* Your app */}
    </ThemeProviderWithLocalStorage>
  );
}

With Cookies + Next.js SSR

Here's an example that uses Cookie storage, and server side logic to load the correct default theme on first load.

import { ThemeProviderWithCookies } from "@/ui/themes/ThemeContextWithCookies";
import { cookies } from "next/headers";
 
const THEME_COOKIE = "devie-theme";
const DEFAULT_THEME = "theme-default";
const ALLOWED_THEMES = new Set([DEFAULT_THEME, "theme-dark", "theme-forest"]);
 
export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  // Read theme from cookie on the server
  const cookieStore = await cookies();
  const cookieTheme = cookieStore.get(THEME_COOKIE)?.value;
  const savedTheme =
    cookieTheme && ALLOWED_THEMES.has(cookieTheme) ? cookieTheme : DEFAULT_THEME;
 
  return (
    <html lang="en" className={savedTheme}>
      <body>
        <ThemeProviderWithCookies defaultTheme={savedTheme}>
          {children}
        </ThemeProviderWithCookies>
      </body>
    </html>
  );
}

Reference

ThemeContext Props

PropTypeDefaultDescription
defaultThemestring"theme-default"The initial theme class name
autoLoadThemebooleanfalseLocalStorage Only: Automatically load saved theme on mount

useTheme() Returns

PropertyTypeDescription
selectedThemestringThe currently active theme
setTheme(theme)(string) => voidSet and persist a new theme
previewTheme(theme)(string) => voidPreview a theme without persisting
clearPreviewTheme()() => voidClear the preview, revert to selected
isThemeLoadedbooleanLocalStorage only: whether theme has loaded
loadInitialTheme()() => voidLocalStorage only: manually trigger theme loading
primaryColorstringComputed primary color from current theme