Theme Toggle

View registry JSON

Switch between a sun and crescent moon inside one circular button.

Light mode

Install with the shadcn CLI

npx shadcn@4.21.5 add @cavemio/theme-toggle
First install? Configure @cavemio once

Run npx shadcn@4.21.5 init if shadcn is not set up, then merge this entry into the registries field in your existing components.json. Keep your other settings and registries.

components.json
{
  "registries": {
    "@cavemio": "https://registry.cavemio.dev/r/cavemio-{name}.json"
  }
}

Overview

Switch between a sun and crescent moon inside one circular button.

Optional pageTransition="circular" reveals the new page theme from the button while the icon morphs. Apply your theme class, data attribute, or synchronous React state inside onThemeChange; the component does not define colors or persist preferences. Without the option, only the icon animates. Unsupported browsers, reduced motion, hidden documents, and failed captures switch immediately. Resize or enabling reduced motion settles an active wipe; repeated activation keeps the latest choice. An iframe only animates its own document. Deferred provider updates and router transitions need app coordination; use pageTransition="none" until integrated. With a strict style CSP, pass an allowed nonce. The demo starts with the wipe enabled. Use the With wipe and Without wipe tabs to compare. On the website, the demo changes the entire site theme and stays synchronized with the header.

"use client";

import { useState } from "react";
import { ThemeToggle } from "@/components/theme-toggle";

// Your CSS must define light/dark colors for this attribute.
export function AppearanceToggle() {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  return (
    <ThemeToggle
      theme={theme}
      pageTransition="circular"
      onThemeChange={(next) => {
        document.documentElement.dataset.theme = next;
        setTheme(next);
      }}
    />
  );
}

Props

PropTypeDescription
theme"light" | "dark"Controlled appearance; pair with onThemeChange.
defaultTheme"light" | "dark"Initial uncontrolled appearance. Defaults to light.
onThemeChange(theme: "light" | "dark") => voidReceives the next appearance when activated. The host owns document theme and persistence.
pageTransition"none" | "circular"Defaults to none. Circular opts into a 620ms document reveal and keeps button scale stable. onThemeChange must apply theme styles synchronously; reduced motion or unavailable transitions switch immediately.
sizenumberButton diameter in pixels. Defaults to 48, with a minimum 44px hit area.
disabledbooleanDisables pointer and keyboard activation.
className / stylestring / CSSPropertiesCustomize the button surface and dimensions.
aria-labelstringOverride the default destination label. Native button props are forwarded.
noncestringOptional CSP nonce for the temporary transition stylesheet. Use your server-generated allowed style nonce.