Theme Components
ThemeProvider, ThemeToggle, and AnimatedThemeToggler.
Theme components
Domain: theme
Package path: packages/ui/src/theme/
Import: @ziteox/ui or @ziteox/ui/theme
Preview
Components
| Component | Purpose |
|---|---|
ThemeProvider | App-level theme context (next-themes wrapper) |
ThemeToggle | Ready-to-use toggle wired to next-themes |
AnimatedThemeToggler | Low-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
| Prop | Default |
|---|---|
attribute | "class" |
defaultTheme | "system" |
enableSystem | true |
disableTransitionOnChange | true |
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
- Before mount: renders
AnimatedThemeTogglerin a static state (isDark={false}, no sound/haptic, no-op toggle) to avoid hydration mismatch. - After mount: reads
resolvedThemefromnext-themesand toggles between"light"and"dark".
Requirements
ThemeProviderancestor- Global
.darkclass CSS (see getting-started.md)
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.
Controlled (recommended with custom state)
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
| Prop | Type | Default | Description |
|---|---|---|---|
isDark | boolean | — | Controlled dark state. Omit for uncontrolled mode. |
onToggle | () => void | — | Click handler. Omit to toggle dark class on <html>. |
sound | boolean | true | Procedural click via Web Audio API |
haptic | boolean | true | Subtle WebHaptics tap (lighter than HamburgerButton); skipped when prefers-reduced-motion |
Animation details
- Spring animation via
motion(iconSpringpreset from internalanimations/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
| Decision | Rationale |
|---|---|
Split ThemeToggle / AnimatedThemeToggler | Convenience vs. flexibility without forcing next-themes on low-level usage |
next-themes as peer | Single theme context per app; app controls version |
motion for this component | SVG morph too complex for maintainable pure CSS |
Controlled props on AnimatedThemeToggler | Parent owns truth; matches Forge controlled-component pattern |