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.jsonnpm package
For centralized upgrades, install the optional ESM package and configure Tailwind source detection as shown below.
pnpm add suluu motionUsage
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.
"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")}
/>
)
}// 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
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.
<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.
| Prop | Type | Default | Description |
|---|---|---|---|
| checked | boolean | — | Controlled state. True represents dark mode. |
| defaultChecked | boolean | false | Initial uncontrolled state. |
| onCheckedChange | (checked) => void | — | Runs when a click or key press requests a state change. |
| motionIntensity | "subtle" | "default" | "expressive" | "default" | Controls icon rotation, contraction, blur, spring, and press response. |
| lightIcon | ReactNode | custom sun | Replaces the light-mode icon. |
| darkIcon | ReactNode | custom moon | Replaces the dark-mode icon. |
| disabled | boolean | false | Disables interaction. |
| aria-label | string | "Dark mode" | Stable accessible name. aria-labelledby is also supported. |
| className | string | — | Class 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.