threecn

Theming

How useShadcnTheme bridges your CSS variables into Three.js.

Every scene colors its materials through one hook: useShadcnTheme(). It reads your shadcn CSS variables, converts them to THREE.Color instances, and keeps them in sync as the theme changes.

How it works

"use client"

import * as React from "react"
import { Color } from "three"

export function useShadcnTheme(mode = "auto") {
  const [colors, setColors] = React.useState(createDefaults)

  React.useEffect(() => {
    // 1. Read CSS variables from <html>.
    const read = () => {
      const styles = getComputedStyle(document.documentElement)
      const primary = styles.getPropertyValue("--primary") // "263 70% 50%"
      // 2. Parse "H S% L%" into a THREE.Color.
      const [h, s, l] = primary.trim().split(/\s+/)
      const color = new Color().setHSL(
        parseFloat(h) / 360,
        parseFloat(s) / 100,
        parseFloat(l) / 100
      )
      setColors((prev) => ({ ...prev, primaryColor: color }))
    }

    read()

    // 3. Re-read whenever the .dark class (or styles) change.
    const observer = new MutationObserver(read)
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class", "style", "data-theme"],
    })
    return () => observer.disconnect()
  }, [mode])

  return colors
}

The real hook also exposes bgColor, foregroundColor, borderColor, mutedColor, accentColor, primaryForegroundColor and an isDark boolean, and is SSR-safe (it returns neutral defaults before the DOM exists).

What it returns

const {
  primaryColor, // THREE.Color from --primary
  bgColor, // --background
  borderColor, // --border
  mutedColor, // --muted-foreground
  accentColor, // --accent
  isDark, // true when <html> has the .dark class
} = useShadcnTheme()

Dark mode

Because the hook watches the .dark class, it works out of the box with next-themes. Toggle the theme and every mounted scene re-reads its colors on the next frame — no remount, no prop changes.

Overriding colors per scene

Two ways to deviate from the live theme:

Force a mode with the theme prop:

<ParticleField theme="dark" /> // always dark palette
<ParticleField theme="light" /> // always light palette
<ParticleField theme="auto" /> // follow the document (default)

Override a specific color — some scenes accept an explicit color that wins over the token:

<ProductShowcase color="#22d3ee" />

Changing tokens at runtime

Since the hook reads live CSS variables, setting one updates every scene:

document.documentElement.style.setProperty("--primary", "160 84% 39%")
// all scenes turn emerald on the next frame

This is exactly how the accent picker on the landing page works.

On this page