Unified UI

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-toggle
pnpm dlx @work-rjkashyap/unified-ui add theme-toggle
npx @work-rjkashyap/unified-ui add theme-toggle
bunx @work-rjkashyap/unified-ui add theme-toggle

If 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-ui
pnpm add @work-rjkashyap/unified-ui
yarn add @work-rjkashyap/unified-ui
bun add @work-rjkashyap/unified-ui

Anatomy

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

PropTypeDefaultDescription
value"light" | "dark" | "system"Required. The current theme value.
onChange(value: ThemeValue) => voidRequired. 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.
classNamestringExtra classes on the outer element.

Variants Reference

VariantDescriptionBest For
iconSingle button that cycles on click with animated icon transitionNavbars, toolbars, compact spaces
segmentedInline pill group with one button per optionSettings pages, preference panels

Accessibility

  • Keyboard: The icon variant is a standard <button> — focusable and activatable with Enter or Space. The segmented variant uses role="radiogroup" with role="radio" on each option, supporting aria-checked.
  • Focus ring: Uses the design system's standard focusRingClasses for visible keyboard focus.
  • Labels: The icon variant sets a descriptive aria-label indicating which theme it will switch to. Each segmented button has an aria-label matching its theme name.
  • Motion: Icon transitions use transition-all with duration-300 and respect prefers-reduced-motion via the standard Tailwind motion-safe / motion-reduce utilities.

Design Tokens

TokenCSS VariableTailwindUsage
Background--secondarybg-secondaryIcon button background
Border--borderborder-borderButton border
Text--secondary-foregroundtext-secondary-foregroundIcon color
Active bg--backgroundbg-backgroundActive segment background
Active text--foregroundtext-foregroundActive segment icon
Muted--mutedbg-mutedSegmented container background
Transition--duration-fastduration-fastHover/active transitions

On this page