Patch UI

ThemeToggle

A light/dark mode toggle button with animated sun/moon icons. Works uncontrolled with localStorage persistence, or controlled via props. Applies the .dark class to <html> by default.

$npx shadcn add @patchui/theme-toggle

Default

Click to toggle the site theme. Persists to localStorage.

Sizes

Controlled

Current: light

Usage

import { ThemeToggle } from "@patchui/react";
 
// Uncontrolled - persists to localStorage, applies .dark class
<ThemeToggle />
 
// Controlled
const [theme, setTheme] = useState<"light" | "dark">("light");
<ThemeToggle
  theme={theme}
  onThemeChange={setTheme}
/>
 
// With next-themes
import { useTheme } from "next-themes";
 
function MyToggle() {
  const { resolvedTheme, setTheme } = useTheme();
  return (
    <ThemeToggle
      theme={resolvedTheme as "light" | "dark"}
      onThemeChange={setTheme}
      applyClass={false}
      storageKey={false}
    />
  );
}

Flash Prevention

Add this inline script to your <head> to prevent a flash of wrong theme on page load:

<script>
  (function() {
    var t = localStorage.getItem('patch-ui-theme');
    if (!t) t = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    if (t === 'dark') document.documentElement.classList.add('dark');
  })();
</script>

Props

PropTypeDefaultDescription
theme"light" | "dark" | "system"-Controlled theme value. Omit for uncontrolled mode.
onThemeChange(theme: ResolvedTheme) => void-Called when the theme changes.
storageKeystring | false"patch-ui-theme"localStorage key for persistence. Set to false to disable.
applyClassbooleantrueWhether to toggle the .dark class on <html>.
size"sm" | "md" | "lg""md"Size of the toggle button.

Notes

  • Uncontrolled mode: By default, ThemeToggle manages its own state, reads from localStorage on mount, and applies the .dark class to <html>. No provider needed.
  • Controlled mode: Pass theme and onThemeChange for external control (e.g. with next-themes). Set applyClass={false} and storageKey={false} to avoid conflicts.
  • SSR safe: Uses useSyncExternalStore to detect mount state. Renders a blank placeholder before hydration to prevent mismatch.
  • Animation: Icon transitions use a CSS keyframe (patch-theme-icon-in) defined in the patch-ui tokens CSS.
  • System preference: Pass theme="system" to resolve based on prefers-color-scheme.