# Theme Toggle

An accessible React dark mode toggle for Next.js that reveals the new theme from the button with a View Transition: a circle, ink, a wave or a page turn.

Source: https://ui.fewtone.com/components/theme-toggle

## Installation

```bash
npx shadcn@latest add https://ui.fewtone.com/r/theme-toggle.json
```

## Usage

```tsx
import { ThemeToggle } from "@/components/ui/theme-toggle"

// Inside next-themes' ThemeProvider (shadcn's dark mode setup)
export function SiteHeader() {
  return (
    <header>
      <ThemeToggle reveal="ink" speed="medium" />
    </header>
  )
}
```

## API reference

### ThemeToggle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `reveal` | `ThemeReveal \| ThemeReveal[] \| "random" \| "none"` | `"circle"` | How the new theme appears: "circle", "shrink", "ink", "tide", "page-turn", "fade", "wipe-right", "wipe-down", "feather", "curtain" or "diagonal". "random" picks a different one each time; a list picks from that list. |
| `speed` | `"quick" \| "medium" \| "slow" \| number` | `"medium"` | How long the reveal takes: 450ms, 700ms, 1000ms, or a number of milliseconds. |
| `variant` | `Button variant` | `"outline"` | The button style. |
| `size` | `Button size` | `"icon"` | The button size. |

### revealTheme

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `theme` | `string` | – | The theme to switch to, for example "dark". |
| `setTheme` | `(theme: string) => void` | – | setTheme from next-themes. |
| `origin` | `{ x: number; y: number }` | – | Where the reveal starts, in viewport pixels. Defaults to the centre of the screen. |
| `reveal` | `ThemeReveal \| ThemeReveal[] \| "random" \| "none"` | `"circle"` | As on ThemeToggle. |
| `speed` | `"quick" \| "medium" \| "slow" \| number` | `"medium"` | As on ThemeToggle. |

**When to use it:** Use ThemeToggle for a one-click light/dark switch in a header. It needs next-themes' ThemeProvider. Use revealTheme to switch from a menu or a shortcut with the same reveal.
**Built on:** next-themes, View Transitions API.
**npm packages (installed by the CLI):** motion@^14.0.0, next-themes@^0.4.6.
**Installs these FewTone UI components with it:** `button`.

## Examples

### All reveals

Press any toggle to switch the theme with that reveal, starting from where you pressed. The speed applies to all of them.

```tsx
"use client";

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import {
  type RevealOption,
  type ThemeRevealSpeed,
  ThemeToggle,
} from "@/components/ui/theme-toggle";
import { useId, useState } from "react";

const reveals: { reveal: RevealOption; label: string }[] = [
  { reveal: "circle", label: "Circle" },
  { reveal: "shrink", label: "Shrink" },
  { reveal: "ink", label: "Ink" },
  { reveal: "tide", label: "Tide" },
  { reveal: "page-turn", label: "Page turn" },
  { reveal: "fade", label: "Fade" },
  { reveal: "wipe-right", label: "Wipe right" },
  { reveal: "wipe-down", label: "Wipe down" },
  { reveal: "feather", label: "Feather" },
  { reveal: "curtain", label: "Curtain" },
  { reveal: "diagonal", label: "Diagonal" },
  { reveal: "random", label: "Random" },
];

const speeds: { speed: ThemeRevealSpeed; label: string }[] = [
  { speed: "quick", label: "Quick · 450ms" },
  { speed: "medium", label: "Medium · 700ms" },
  { speed: "slow", label: "Slow · 1000ms" },
];

/*
 * Fills the preview: the speed setting in the top-right corner, the toggles in a compact grid in the middle.
 * An empty row at the bottom, as tall as the setting's row, keeps the grid truly centred.
 */
export default function ThemeToggleReveals() {
  const id = useId();
  const [speed, setSpeed] = useState<ThemeRevealSpeed>("medium");

  return (
    <div className="flex w-full flex-col gap-6 self-stretch">
      <div className="-mt-6 -mr-6 flex items-center justify-end gap-2">
        <label htmlFor={`${id}-speed`} className="text-sm text-muted-foreground">
          Speed
        </label>
        <Select value={speed} onValueChange={(value) => setSpeed(value as ThemeRevealSpeed)}>
          <SelectTrigger id={`${id}-speed`} size="sm" className="w-36">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            {speeds.map((option) => (
              <SelectItem key={option.speed} value={option.speed}>
                {option.label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
      </div>

      <div className="flex flex-1 items-center justify-center">
        <ul className="grid w-fit grid-cols-3 gap-x-3 gap-y-5 sm:grid-cols-4 lg:grid-cols-6">
          {reveals.map(({ reveal, label }) => (
            <li key={label} className="flex flex-col items-center gap-1.5">
              <ThemeToggle reveal={reveal} speed={speed} aria-describedby={`${id}-${label}`} />
              <span id={`${id}-${label}`} className="text-xs whitespace-nowrap text-muted-foreground">
                {label}
              </span>
            </li>
          ))}
        </ul>
      </div>

      <div aria-hidden className="-mb-6 h-(--size-control-sm)" />
    </div>
  );
}
```

### With a switch

revealTheme works with any control. Here a Switch turns dark mode on and off, and the new theme grows out of it.

```tsx
"use client";

import { Switch } from "@/components/ui/switch";
import { revealTheme } from "@/components/ui/theme-toggle";
import { cn } from "@/lib/utils";
import { MoonIcon, SunIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { useId, useRef, useSyncExternalStore } from "react";

const noop = () => () => {};

export default function ThemeToggleSwitch() {
  const id = useId();
  const control = useRef<HTMLButtonElement>(null);
  const { resolvedTheme, setTheme } = useTheme();
  // The theme is only known in the browser; until then the switch shows off
  const mounted = useSyncExternalStore(
    noop,
    () => true,
    () => false,
  );
  const dark = mounted && resolvedTheme === "dark";

  return (
    <div className="flex items-center gap-3">
      <label htmlFor={id} className="mr-2 text-sm font-medium">
        Dark mode
      </label>
      <SunIcon
        aria-hidden
        className={cn(
          "size-(--size-icon-md) transition-colors duration-(--duration-base)",
          dark ? "text-muted-foreground" : "text-foreground",
        )}
      />
      <Switch
        id={id}
        ref={control}
        checked={dark}
        onCheckedChange={(checked) => {
          // The new theme grows out of the switch
          const box = control.current?.getBoundingClientRect();
          revealTheme({
            theme: checked ? "dark" : "light",
            setTheme,
            origin: box ? { x: box.left + box.width / 2, y: box.top + box.height / 2 } : undefined,
            reveal: "circle",
          });
        }}
      />
      <MoonIcon
        aria-hidden
        className={cn(
          "size-(--size-icon-md) transition-colors duration-(--duration-base)",
          dark ? "text-foreground" : "text-muted-foreground",
        )}
      />
    </div>
  );
}
```

## Related

- [Button](https://ui.fewtone.com/components/button.md): An accessible React button for Next.js and Tailwind CSS with six variants, three sizes, icons and a soft tint on hover and press, built on a native button.
- [Switch](https://ui.fewtone.com/components/switch.md): An accessible React switch for on/off settings, with a spring-animated thumb, built on Radix Switch.
