Theme switching at runtime
helixui themes are runtime-switchable. The cascade does the work — no re-render storm, no FOUC if you do the SSR step right.
Browser-only (SPA / Vite)
The simplest pattern. Read once, hydrate.
'use client';
import { useEffect, useState } from 'react';import { HelixUIDNAProvider } from '@helixui/core';import { wildtype, PRESETS, type DNA } from '@helixui/dna';
function readSavedDna(): DNA { if (typeof localStorage === 'undefined') return wildtype(); const raw = localStorage.getItem('helixui:dna'); if (!raw) return wildtype(); try { return JSON.parse(raw); } catch { return wildtype(); }}
export function ThemeRoot({ children }: { children: React.ReactNode }) { const [dna, setDna] = useState<DNA>(() => readSavedDna()); useEffect(() => { localStorage.setItem('helixui:dna', JSON.stringify(dna)); }, [dna]); return ( <HelixUIDNAProvider dna={dna}> <ThemeContext.Provider value={{ dna, setDna }}>{children}</ThemeContext.Provider> </HelixUIDNAProvider> );}To switch:
const { setDna } = useTheme();setDna(PRESETS.noir());Next.js (App Router) — no flash
The flash you usually see (“page renders light, then switches to dark
after JS loads”) happens because the server doesn’t know the user’s
preference. Use a cookie + a tiny inline script to set data-theme
before React hydrates:
import { cookies } from 'next/headers';
const FLASH_GUARD = ` (() => { try { const dna = JSON.parse(document.cookie.replace(/(?:(?:^|.*;\\s*)helixui-dna\\s*=\\s*([^;]*).*$)|^.*$/, '$1') || 'null'); if (dna && dna.genes && dna.genes.lightness) { document.documentElement.dataset.theme = dna.genes.lightness.variant; } } catch {} })();`;
export default function RootLayout({ children }: { children: React.ReactNode }) { const dna = JSON.parse(cookies().get('helixui-dna')?.value ?? '"wildtype"'); return ( <html lang="en"> <head> <script dangerouslySetInnerHTML={{ __html: FLASH_GUARD }} /> </head> <body> <ThemeRoot initial={dna}>{children}</ThemeRoot> </body> </html> );}The inline script runs before hydration, sets <html data-theme>,
and helixui’s CSS picks up the right semantic tokens immediately.
Account-synced themes
When you persist a user’s DNA on the server, the flow is:
- Server fetches the user’s DNA from the database.
- Server passes it as a prop to
<ThemeRoot>. - Client mutations (DNA Lab edits) call your
PATCH /api/me/dnaendpoint and update local state. HelixUIDNAProviderre-renders; the cascade updates instantly.
// app/api/me/dna/route.ts (Next.js App Router)export async function PATCH(req: Request) { const dna = await req.json(); await db.user.update({ where: { id }, data: { dna } }); return Response.json({ ok: true });}// clientconst saveDna = async (next: DNA) => { setDna(next); // optimistic try { await fetch('/api/me/dna', { method: 'PATCH', body: JSON.stringify(next) }); } catch { setDna(previous); // rollback toast.error('Could not save theme'); }};Multiple themes on one page
Wrap subtrees in their own <HelixUIDNAProvider>. The provider compiles
the DNA into a scoped CSS-var block; nesting Just Works.
<HelixUIDNAProvider dna={PRESETS.studio()}> <Dashboard /> <HelixUIDNAProvider dna={PRESETS.noir()}> <Card>{/* this card is themed noir; everything else is studio */}</Card> </HelixUIDNAProvider></HelixUIDNAProvider>Useful for: theme previews, in-app design playground, A/B test surfaces.
Performance
- One DNA → one
<style>block ≈ ~80 CSS variable declarations. - Switching DNA = re-emit the block. Browsers handle this fast (sub-frame on a modern laptop).
- No JS recomputation in components; React doesn’t re-render unless
your code reads the DNA via
useDna().
If you measure layout shifts, profile, and find DNA switching is the bottleneck — open an issue. Pre-1.0 we want every report.
What we don’t do
- We don’t ship a “light/dark toggle” button. That’s a 5-line snippet
and it constrains your IA decisions. We give you
lightness: 'light' | 'dark' | 'auto'and let you decide where the toggle lives. - We don’t sync to
prefers-color-schemeautomatically when the user has explicitly chosen a theme. Auto-mode (the default) does; explicit picks win.