Theme toggles
Ready-made theme switchers built on useTheme. Install them with the shadcn CLI or copy the code.
Each toggle is a single client component that reads and updates the theme with useTheme from @wrksz/themes/client. They use shadcn/ui color tokens (bg-accent, text-muted-foreground, border-input), so they drop into a shadcn project as-is. Without shadcn, copy the file and map those tokens to your own colors.
The previews on this page control this site's theme.
Add a toggle with an AI agent
A ready prompt that picks a toggle for your UI, swaps the icons for your icon library and matches your colors. Paste it into Claude Code, Codex or any agent working in your repository.
Icons
All toggles import their icons from one file, theme-icons.tsx. The shadcn CLI installs it with the first toggle you add. It uses Hugeicons by default; to use another icon library, change the four exports in this file and the toggles follow. In a shadcn project, npx shadcn@latest migrate icons components/theme-icons.tsx does it for you and asks which library to switch to.
import { ArrowDown01Icon, ComputerIcon, Moon02Icon, Sun03Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
type IconProps = { className?: string };
export function SunIcon({ className }: IconProps) {
return (
<HugeiconsIcon
icon={Sun03Icon}
color="currentColor"
strokeWidth={1.8}
aria-hidden
className={className}
/>
);
}
export function MoonIcon({ className }: IconProps) {
return (
<HugeiconsIcon
icon={Moon02Icon}
color="currentColor"
strokeWidth={1.8}
aria-hidden
className={className}
/>
);
}
export function SystemIcon({ className }: IconProps) {
return (
<HugeiconsIcon
icon={ComputerIcon}
color="currentColor"
strokeWidth={1.8}
aria-hidden
className={className}
/>
);
}
export function ChevronDownIcon({ className }: IconProps) {
return (
<HugeiconsIcon
icon={ArrowDown01Icon}
color="currentColor"
strokeWidth={2}
aria-hidden
className={className}
/>
);
}Theme toggle
An icon button that switches between light and dark. The icons swap with Tailwind's dark: variant instead of React state, so the right icon shows before hydration.
pnpm dlx shadcn@latest add https://themes.wrksz.dev/r/theme-toggle.json"use client";
import { useTheme } from "@wrksz/themes/client";
import { MoonIcon, SunIcon } from "./theme-icons";
const SWAP =
"absolute size-4 transition-[opacity,scale,filter] duration-300 ease-[cubic-bezier(0.2,0,0,1)] motion-reduce:transition-opacity";
export function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
const dark = resolvedTheme === "dark";
return (
<button
type="button"
onClick={() => setTheme(dark ? "light" : "dark")}
aria-label={dark ? "Switch to light theme" : "Switch to dark theme"}
className="relative inline-flex size-9 items-center justify-center rounded-md text-muted-foreground transition-[color,background-color,scale] duration-150 ease-out hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none active:scale-[0.96]"
>
<SunIcon className={`${SWAP} dark:scale-50 dark:opacity-0 dark:blur-[2px]`} />
<MoonIcon
className={`${SWAP} scale-50 opacity-0 blur-[2px] dark:scale-100 dark:opacity-100 dark:blur-none`}
/>
</button>
);
}Theme switcher
A segmented control for light, system and dark. The indicator stays hidden until the stored theme is known, so it never slides in from the wrong option.
pnpm dlx shadcn@latest add https://themes.wrksz.dev/r/theme-switcher.json"use client";
import { useTheme } from "@wrksz/themes/client";
import { MoonIcon, SunIcon, SystemIcon } from "./theme-icons";
const OPTIONS = [
{ value: "light", label: "Light", Icon: SunIcon },
{ value: "system", label: "System", Icon: SystemIcon },
{ value: "dark", label: "Dark", Icon: MoonIcon },
] as const;
export function ThemeSwitcher() {
const { theme, setTheme } = useTheme();
const index = OPTIONS.findIndex((option) => option.value === theme);
return (
<fieldset
aria-label="Theme"
className="relative inline-flex rounded-full border border-border bg-background p-0.5"
>
<span
aria-hidden
className="absolute top-0.5 left-0.5 size-7 rounded-full bg-accent transition-[translate,opacity] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none"
style={{ translate: `${Math.max(index, 0) * 100}% 0`, opacity: index < 0 ? 0 : 1 }}
/>
{OPTIONS.map(({ value, label, Icon }) => (
<button
key={value}
type="button"
aria-pressed={theme === value}
aria-label={label}
title={label}
onClick={() => setTheme(value)}
className="relative inline-flex size-7 items-center justify-center rounded-full text-muted-foreground transition-colors duration-150 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none aria-pressed:text-foreground"
>
<Icon className="size-3.5" />
</button>
))}
</fieldset>
);
}Theme select
A native <select>, so keyboard and screen reader support come from the browser. It stays disabled until the theme is known.
pnpm dlx shadcn@latest add https://themes.wrksz.dev/r/theme-select.json"use client";
import { useTheme } from "@wrksz/themes/client";
import { ChevronDownIcon } from "./theme-icons";
const THEMES = ["light", "dark", "system"] as const;
export function ThemeSelect() {
const { theme, setTheme } = useTheme();
return (
<label className="relative inline-flex items-center">
<span className="sr-only">Theme</span>
<select
value={theme ?? "system"}
disabled={!theme}
onChange={(event) => {
const next = THEMES.find((value) => value === event.target.value);
if (next) setTheme(next);
}}
className="h-9 appearance-none rounded-md border border-input bg-background ps-3 pe-8 text-sm text-foreground transition-colors duration-150 hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:opacity-50"
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
<ChevronDownIcon className="pointer-events-none absolute end-2.5 size-3.5 text-muted-foreground" />
</label>
);
}Theme cycle
A button that cycles light, dark and system and shows the current choice as text.
pnpm dlx shadcn@latest add https://themes.wrksz.dev/r/theme-cycle.json"use client";
import { useTheme } from "@wrksz/themes/client";
import { MoonIcon, SunIcon, SystemIcon } from "./theme-icons";
const ORDER = ["light", "dark", "system"] as const;
const OPTIONS = {
light: { label: "Light", Icon: SunIcon },
dark: { label: "Dark", Icon: MoonIcon },
system: { label: "System", Icon: SystemIcon },
};
export function ThemeCycle() {
const { theme, setTheme } = useTheme();
const current = ORDER.find((value) => value === theme);
const next = ORDER[(ORDER.indexOf(current ?? "system") + 1) % ORDER.length];
const Icon = current ? OPTIONS[current].Icon : null;
return (
<button
type="button"
onClick={() => setTheme(next)}
aria-label={`Theme: ${current ? OPTIONS[current].label : "loading"}. Switch to ${OPTIONS[next].label}`}
className="inline-flex h-9 min-w-27 items-center gap-2 rounded-md border border-input bg-background px-3 text-sm text-foreground transition-[background-color,scale] duration-150 ease-out hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none active:scale-[0.97]"
>
{Icon ? <Icon className="size-4 text-muted-foreground" /> : <span className="size-4" />}
<span>{current ? OPTIONS[current].label : "Theme"}</span>
</button>
);
}Requirements
- A
ThemeProviderabove the toggle. enableSystemfor the system option in the switcher, select and cycle. It is on by default.- For the theme toggle,
attribute="class"with Tailwind'sdark:variant on the.darkclass, the shadcn default.