ThemeToggle
A light/dark/system mode switcher with icon and segmented variants. Headless and controlled — works with any theme provider.
Overview
The ThemeToggle component is a controlled light/dark/system mode switcher. It supports two visual variants — a single icon button that cycles on click, and a segmented pill group with one button per option. It's fully headless: you provide the current value and an onChange callback, so it works with any theme provider (next-themes, custom context, etc.).
Installation
Install the component via the CLI in one command.
npx @work-rjkashyap/unified-ui add theme-togglepnpm dlx @work-rjkashyap/unified-ui add theme-togglenpx @work-rjkashyap/unified-ui add theme-togglebunx @work-rjkashyap/unified-ui add theme-toggleIf you haven't initialized your project yet, run npx @work-rjkashyap/unified-ui init first. See the CLI docs for details.
Or install the full package
Use this approach if you prefer to install the entire design system as a dependency instead of copying individual components.
npm install @work-rjkashyap/unified-uipnpm add @work-rjkashyap/unified-uiyarn add @work-rjkashyap/unified-uibun add @work-rjkashyap/unified-uiAnatomy
import { ThemeToggle } from "@work-rjkashyap/unified-ui";Examples
Icon Variant (Default)
Segmented Variant
Three-State Mode (Light / Dark / System)
Sizes
Integration with next-themes
The most common integration is with next-themes:
"use client";
import { useTheme } from "next-themes";
import { ThemeToggle, type ThemeValue } from "@work-rjkashyap/unified-ui";
import { useEffect, useState } from "react";
export function SiteThemeToggle() {
const { theme, setTheme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null;
// For 2-state mode, use resolvedTheme (never "system")
// For 3-state mode, use theme (can be "system")
return (
<ThemeToggle
value={(theme ?? "system") as ThemeValue}
onChange={setTheme}
mode="light-dark-system"
variant="segmented"
/>
);
}Props
ThemeToggle
| Prop | Type | Default | Description |
|---|---|---|---|
value | "light" | "dark" | "system" | — | Required. The current theme value. |
onChange | (value: ThemeValue) => void | — | Required. Callback when the theme changes. |
mode | "light-dark" | "light-dark-system" | "light-dark" | Whether to include the system/auto option. |
variant | "icon" | "segmented" | "icon" | Visual variant. |
size | "sm" | "md" | "lg" | "md" | Size of the toggle. |
className | string | — | Extra classes on the outer element. |
Variants Reference
| Variant | Description | Best For |
|---|---|---|
icon | Single button that cycles on click with animated icon transition | Navbars, toolbars, compact spaces |
segmented | Inline pill group with one button per option | Settings pages, preference panels |
Accessibility
- Keyboard: The icon variant is a standard
<button>— focusable and activatable withEnterorSpace. The segmented variant usesrole="radiogroup"withrole="radio"on each option, supportingaria-checked. - Focus ring: Uses the design system's standard
focusRingClassesfor visible keyboard focus. - Labels: The icon variant sets a descriptive
aria-labelindicating which theme it will switch to. Each segmented button has anaria-labelmatching its theme name. - Motion: Icon transitions use
transition-allwithduration-300and respectprefers-reduced-motionvia the standard Tailwindmotion-safe/motion-reduceutilities.
Design Tokens
| Token | CSS Variable | Tailwind | Usage |
|---|---|---|---|
| Background | --secondary | bg-secondary | Icon button background |
| Border | --border | border-border | Button border |
| Text | --secondary-foreground | text-secondary-foreground | Icon color |
| Active bg | --background | bg-background | Active segment background |
| Active text | --foreground | text-foreground | Active segment icon |
| Muted | --muted | bg-muted | Segmented container background |
| Transition | --duration-fast | duration-fast | Hover/active transitions |