# Theme Toggle

A controlled light/dark switch with optional view-transition effects.

Page: https://sui.draco.dev/docs/components/theme-toggle

### Example: theme-toggle-demo

```tsx
"use client";
import { ThemeToggle } from "@workspace/ui/components/theme-toggle";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { applyAccent, normalizeHex, themePresets } from "../../lib/theme";
import type { ExampleProps } from "../types";

export default function Example({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  const themeLabels = chinese
    ? { light: "浅色", dark: "深色" }
    : { light: "Light", dark: "Dark" };
  const { resolvedTheme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  const theme = mounted && resolvedTheme === "dark" ? "dark" : "light";
  const changeTheme = (next: "light" | "dark") => {
    const root = document.documentElement;
    const accent =
      root.dataset.color === "custom"
        ? normalizeHex(root.dataset.colorSeed ?? "")
        : (themePresets.find((preset) => preset.id === root.dataset.color)
            ?.color ?? null);
    root.classList.toggle("dark", next === "dark");
    root.style.colorScheme = next;
    applyAccent(accent, next === "dark");
    setTheme(next);
  };
  return (
    <div className="w-full">
      <div className="flex items-center justify-between gap-4 rounded-xl border bg-background p-6 text-foreground">
        <div className="grid gap-1">
          <p>{chinese ? "文档站主题" : "Documentation appearance"}</p>
          <output className="text-muted-foreground text-sm" aria-live="polite">
            {themeLabels[theme]}
          </output>
        </div>
        <ThemeToggle
          theme={theme}
          onThemeChange={changeTheme}
          disabled={!mounted}
          lightLabel={chinese ? "切换为浅色模式" : "Switch to light mode"}
          darkLabel={chinese ? "切换为深色模式" : "Switch to dark mode"}
        />
      </div>
      <p className="mt-3 text-muted-foreground text-sm">
        {chinese
          ? "此示例切换文档站主题并保存偏好，可在顶栏选择跟随系统或调整配色"
          : "This example switches the documentation theme and saves the preference. The header provides System mode and accent settings."}
      </p>
    </div>
  );
}
```

## Installation

```bash
bunx --bun shadcn@latest add @sui/theme-toggle
```

Install with the shadcn CLI or use the shared `@workspace/ui` package. Follow the [installation guide](/docs/installation) to configure the registry, load styles, and choose import aliases.

## Usage

```tsx
import { ThemeToggle } from "@workspace/ui/components/theme-toggle";
import { useState } from "react";

export function Appearance() {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  return <ThemeToggle theme={theme} onThemeChange={setTheme} />;
}
```

## Controlled appearance

`theme` is the current resolved light or dark theme. `onThemeChange` requests the other theme. Apply the new theme to your application in this callback; the component does not set a provider, save preferences, or implement a System preference. The live examples use the documentation’s `next-themes` provider and change the whole site appearance, saving the selected mode while retaining its accent color. The header still offers System mode and accent settings.

For a System setting, resolve it in your application and pass the effective light or dark theme here. Keep your application’s preference controls separate when System must remain selectable.

## Transition variants

Choose `rectangle`, `circle`, `circle-blur`, or `blinds`. The `start` option selects an origin such as the clicked button, a corner, the center, or a bottom-up transition. These effects use the browser View Transition API. Without support, or with reduced motion, the component changes the theme directly.

### Example: theme-toggle-variants

```tsx
"use client";
import { ThemeToggle } from "@workspace/ui/components/theme-toggle";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { applyAccent, normalizeHex, themePresets } from "../../lib/theme";
import type { ExampleProps } from "../types";

const variants = ["rectangle", "circle", "circle-blur", "blinds"] as const;
export default function Example({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  const { resolvedTheme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  const theme = mounted && resolvedTheme === "dark" ? "dark" : "light";
  const changeTheme = (next: "light" | "dark") => {
    const root = document.documentElement;
    const accent =
      root.dataset.color === "custom"
        ? normalizeHex(root.dataset.colorSeed ?? "")
        : (themePresets.find((preset) => preset.id === root.dataset.color)
            ?.color ?? null);
    root.classList.toggle("dark", next === "dark");
    root.style.colorScheme = next;
    applyAccent(accent, next === "dark");
    setTheme(next);
  };
  return (
    <div className="w-full">
      <div className="grid grid-cols-2 gap-4 rounded-xl border bg-background p-5 text-foreground sm:grid-cols-4">
        {variants.map((variant) => (
          <div key={variant} className="grid justify-items-center gap-2">
            <ThemeToggle
              theme={theme}
              onThemeChange={changeTheme}
              disabled={!mounted}
              variant={variant}
              start="button"
              lightLabel={chinese ? "切换为浅色模式" : "Switch to light mode"}
              darkLabel={chinese ? "切换为深色模式" : "Switch to dark mode"}
            />
            <span className="text-muted-foreground text-xs">{variant}</span>
          </div>
        ))}
      </div>
    </div>
  );
}
```

## Labels and icons

`lightLabel` and `darkLabel` describe the action to switch to each theme and supply the tooltip text. The default ghost button uses a 16px Sun/Moon SVG with 2px strokes and a snappy spring geometry morph. Reduced motion swaps the shape directly. Customize `lightIcon`, `darkIcon`, and `iconClassName` when needed; arbitrary React icons switch directly rather than undergoing a path morph. Native button props, including `disabled`, `ref`, and `render`, are supported. The switch does not submit a surrounding form.

## API reference

| Prop | Type | Default / behavior |
| --- | --- | --- |
| `theme` | `"light" \| "dark"` | Required resolved theme. |
| `onThemeChange` | `(theme: "light" \| "dark") => void` | Required theme update callback. |
| `variant` | `"rectangle" \| "circle" \| "circle-blur" \| "blinds"` | `"rectangle"`. |
| `start` | `"button" \| "top-left" \| "top-right" \| "bottom-left" \| "bottom-right" \| "center" \| "bottom-up"` | `"bottom-up"`. |
| `lightLabel`, `darkLabel` | `string` | English switch-action labels. |
| `lightIcon`, `darkIcon` | `ReactNode` | Default theme icons. |
| `iconClassName` | `string` | Optional icon styling. |
| `glass` | `boolean` | `false`. |

The module exports `ThemeToggleProps`. See [View Transition API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) for browser transition behavior.
