Skip to content
Suluu

ThemeToggle

A compact theme control centered on one carefully restrained gesture. Its custom sun and crescent trade places through a soft crossfade, slight counter-rotation, and controlled spring settlement while the surface stays quiet enough for navigation bars and settings rows.

Preview

Switch the site theme

Installation

Registry — recommended

Copies the component into your configured shadcn UI directory and installs Motion.

npx shadcn@latest add https://suluu.site/r/theme-toggle.json

npm package

For centralized upgrades, install the optional ESM package and configure Tailwind source detection as shown below.

pnpm add suluu motion

Usage

The component owns only its boolean state and presentation. Pair it with your theme provider, document class, or persisted setting. A checked toggle represents dark mode. ThemeToggle renders only the circular control; visible labels and status text belong to your surrounding markup.

Registry
"use client"

import { useTheme } from "next-themes"
import { ThemeToggle } from "@/components/ui/theme-toggle"

export function AppThemeToggle() {
  const { resolvedTheme, setTheme } = useTheme()
  const isDark = resolvedTheme === "dark"

  return (
    <ThemeToggle
      checked={isDark}
      disabled={resolvedTheme === undefined}
      onCheckedChange={(dark) => setTheme(dark ? "dark" : "light")}
    />
  )
}
npm
// app/globals.css
@import "suluu/styles.css";
@source "../node_modules/suluu/dist";

// component.tsx
import { ThemeToggle } from "suluu/theme-toggle"

export function AppearanceSetting() {
  return <ThemeToggle defaultChecked />
}

Controlled state

Controlled
const [isDark, setIsDark] = useState(false)

<ThemeToggle
  checked={isDark}
  onCheckedChange={setIsDark}
  motionIntensity="subtle"
/>

Custom icons

Pass any React nodes. Suluu keeps ownership of the transition wrappers, so custom artwork receives the same crossfade and spring treatment without being cloned or restyled.

Icon overrides
<ThemeToggle
  lightIcon={<BrandDayIcon />}
  darkIcon={<BrandNightIcon />}
/>

In context

A theme toggle stays visually light beside a persistent appearance setting.

Dark appearance

Use a quieter palette in low-light spaces.

Props

ThemeToggle accepts safe native button attributes, always uses type="button", and forwards its ref to the button element.

PropTypeDefaultDescription
checkedbooleanControlled state. True represents dark mode.
defaultCheckedbooleanfalseInitial uncontrolled state.
onCheckedChange(checked) => voidRuns when a click or key press requests a state change.
motionIntensity"subtle" | "default" | "expressive""default"Controls icon rotation, contraction, blur, spring, and press response.
lightIconReactNodecustom sunReplaces the light-mode icon.
darkIconReactNodecustom moonReplaces the dark-mode icon.
disabledbooleanfalseDisables interaction.
aria-labelstring"Dark mode"Stable accessible name. aria-labelledby is also supported.
classNamestringClass name applied to the button.

Theming

Override these variables in your light and dark theme scopes. The registry installs the defaults automatically.

--suluu-theme-toggle-backgroundResting button surface.
--suluu-theme-toggle-hoverHovered button surface.
--suluu-theme-toggle-borderHairline border.
--suluu-theme-toggle-sunDefault light-mode icon color.
--suluu-theme-toggle-moonDefault dark-mode icon color.
--suluu-theme-toggle-ringKeyboard focus ring.
--suluu-theme-toggle-offsetColor behind the focus ring offset.
--suluu-theme-toggle-shadowQuiet surface depth.

The root exposes data-state as light or dark for application-specific styling.

Accessibility

The control is a native toggle button with a live aria-pressed state, so Enter and Space work without custom keyboard handling. Keep its accessible name stable; the pressed state communicates whether dark mode is active.

When reduced motion is preferred, icon rotation, contraction, blur, and press scaling are removed. Only a short opacity crossfade remains.

↑↓ to navigate↵ to openesc to close