Ziteox Forge

Theme Components

ThemeProvider, ThemeToggle, and AnimatedThemeToggler.

Theme components

Domain: theme
Package path: packages/ui/src/theme/
Import: @ziteox/ui or @ziteox/ui/theme

Preview

ThemeToggle

Components

ComponentPurpose
ThemeProviderApp-level theme context (next-themes wrapper)
ThemeToggleReady-to-use toggle wired to next-themes
AnimatedThemeTogglerLow-level animated sun/moon control

ThemeProvider

Wraps next-themes with Forge defaults.

import { ThemeProvider } from "@ziteox/ui";

export function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

Defaults

PropDefault
attribute"class"
defaultTheme"system"
enableSystemtrue
disableTransitionOnChangetrue

Types

Exports ThemeProviderProps from next-themes. All next-themes options are supported via spread.

Client component

Includes "use client". Required for next-themes context.


ThemeToggle

High-level toggle. Handles hydration and connects to useTheme().

import { ThemeToggle } from "@ziteox/ui";

export function Header() {
  return (
    <header>
      <ThemeToggle />
    </header>
  );
}

Behavior

  1. Before mount: renders AnimatedThemeToggler in a static state (isDark={false}, no sound/haptic, no-op toggle) to avoid hydration mismatch.
  2. After mount: reads resolvedTheme from next-themes and toggles between "light" and "dark".

Requirements

Props

None. Fully opinionated convenience component.

Client component

Yes ("use client").


AnimatedThemeToggler

Animated sun/moon button. Uses motion/react for SVG animation, Web Audio API for optional click sound, and web-haptics for a light tap on supported mobile devices.

import { AnimatedThemeToggler } from "@ziteox/ui";
import { useTheme } from "next-themes";

function CustomToggle() {
  const { resolvedTheme, setTheme } = useTheme();
  const isDark = resolvedTheme === "dark";

  return (
    <AnimatedThemeToggler
      isDark={isDark}
      onToggle={() => setTheme(isDark ? "light" : "dark")}
      sound={false}
    />
  );
}

Uncontrolled (direct DOM class toggle)

Omit isDark and onToggle to toggle document.documentElement.classList directly:

<AnimatedThemeToggler />

Use only when not using next-themes.

Props

PropTypeDefaultDescription
isDarkbooleanControlled dark state. Omit for uncontrolled mode.
onToggle() => voidClick handler. Omit to toggle dark class on <html>.
soundbooleantrueProcedural click via Web Audio API
hapticbooleantrueSubtle WebHaptics tap (lighter than HamburgerButton); skipped when prefers-reduced-motion

Animation details

  • Spring animation via motion (iconSpring preset from internal animations/springs.ts)
  • Skips animation on first render (useSkipInitialAnimation) to prevent hydration flash
  • Inline SVG sun/moon with mask morph
  • Scoped styles: ziteox-theme-toggler, --ziteox-theme-toggler-ink

Audio

Sound is synthesized at runtime in theme/play-tick.ts. No audio files bundled. Fails silently if Web Audio is unavailable.

Client component

Yes ("use client").


Architecture notes

DecisionRationale
Split ThemeToggle / AnimatedThemeTogglerConvenience vs. flexibility without forcing next-themes on low-level usage
next-themes as peerSingle theme context per app; app controls version
motion for this componentSVG morph too complex for maintainable pure CSS
Controlled props on AnimatedThemeTogglerParent owns truth; matches Forge controlled-component pattern