A button that cycles the theme, its icon turning into the next one.
Installation
Usage
Bring your own theme state. With next-themes:
import { useTheme } from 'next-themes';
import {
ThemeTogglerButton,
ThemeTogglerButtonIcon,
type Resolved,
type ThemeSelection,
} from '@/components/animate-ui/buttons/theme-toggler';
export function ThemeButton() {
const { theme, resolvedTheme, setTheme } = useTheme();
return (
<ThemeTogglerButton
theme={theme as ThemeSelection}
resolvedTheme={resolvedTheme as Resolved}
setTheme={setTheme}
modes={['light', 'dark']}
aria-label="Toggle theme"
>
<ThemeTogglerButtonIcon light={<SunIcon />} dark={<MoonIcon />} />
</ThemeTogglerButton>
);
}Each click applies the next theme of modes at once, through setTheme. For a wipe across the page, use Theme Toggler. When modes leave out system, the button shows the theme actually displayed, and a system theme counts as the one it resolves to. Read the state in your own parts with useThemeTogglerButton().
Animation
- The icon swaps as the theme changes: the old one turns 90° on as it shrinks to half its size into a light blur (
blur(2px)) and fades out, while the new one turns in from 90° back and grows out of it, so both keep turning the same way. Over 0.3s with an ease-out (cubic-bezier(0.23, 1, 0.32, 1)), twice the Copy Button's swap, so the turn reads. Icons only swap once the button is used: on mount, and when the theme becomes known after hydrating, the icon shows at rest. - Pressing scales the button to
tapScale(0.97), on Motion's spring for scale (stiffness: 550, damping: 30). It doesn't scale on hover, unless you sethoverScale. <AnimateUIProvider tempo>paces the swap and the press spring.- Under reduced motion, the icons crossfade through their blur without turning or scaling, and the button doesn't scale.
API Reference
ThemeTogglerButton
| Prop | Type | Default |
|---|---|---|
variant? | 'default' | 'accent' | 'outline' | 'ghost' | null | 'default' |
size? | 'default' | 'sm' | 'lg' | null | 'default' |
theme | 'light' | 'dark' | 'system' | - |
resolvedTheme | 'light' | 'dark' | - |
setTheme | (theme: 'light' | 'dark' | 'system') => void | - |
modes? | ('light' | 'dark' | 'system')[] | ['light', 'dark', 'system'] |
hoverScale? | number | 1 |
tapScale? | number | 0.97 |
transition? | Transition | { type: 'spring', stiffness: 550, damping: 30, restSpeed: 10 } |
asChild? | boolean | false |
...props? | HTMLMotionProps<'button'> | - |
ThemeTogglerButtonIcon
| Prop | Type | Default |
|---|---|---|
light | React.ReactNode | - |
dark | React.ReactNode | - |
system? | React.ReactNode | - |
rotate? | number | 90 |
transition? | Transition | { duration: 0.3, ease: [0.23, 1, 0.32, 1] } |
...props? | React.ComponentProps<'span'> | - |
Data attributes
| Prop | Type | Default |
|---|---|---|
data-theme? | 'light' | 'dark' | 'system' | - |