One design token, two worlds — theming the UI and a 3D scene together
How to make light/dark mode and accent colours drive both Tailwind and a Three.js scene from a single source of truth.
Most portfolio sites that add 3D end up with two colour systems: one in CSS for the interface, and a second, hard-coded one inside the WebGL scene. The moment you add dark mode or let visitors pick an accent, the two drift apart.
The fix is simple: CSS custom properties are the single source of truth, and the 3D scene reads them.
1. Define tokens once
:root { --accent: #b42318; --accent-2: #c2410c; --scene-bg: #faf6f1; }
[data-theme="dark"] { --accent: #e5483a; --accent-2: #f28a2e; --scene-bg: #120b08; }
Tailwind v4 maps them to utilities with @theme inline, so bg-accent and text-fg always follow the active theme.
2. Let the scene observe the tokens
Inside React Three Fiber, read the computed values and watch the <html> element for theme changes:
const observer = new MutationObserver(() => setColors(readColors()));
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme", "data-accent"],
});
3. Ease, don't snap
Instead of swapping material colours instantly, lerp them a little every frame. The whole scene fades into the new palette alongside the CSS transition — it feels deliberate rather than glitchy.
Why it matters
- One place to re-brand. Change a hex value, and the buttons, gradients, particles and reflections all follow.
- No drift. New components can't accidentally use an off-palette colour.
- Drop-in compatible. Keep token names stable and you can swap entire palettes without touching components.
This site uses exactly this approach — try the appearance menu in the top bar.