Split
The next appearance opens from a slim center seam.
themeappearancemotion
Preview
Split preview
Live · keyboard ready
When to use
- Centered layouts where the theme should open symmetrically from a middle seam.
- Pages where the switch position varies, since the split does not depend on where the button sits.
When not to use
- Use theme-switch for a reveal that grows from the button itself.
- Use theme-switch-rise for the calmest option in product UI.
shadcn CLI
Also installs (2)
- manicat-foundation
- button
Manual install(+)
registry.jsonjson
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "theme-switch-split",
"type": "registry:ui",
"title": "Split",
"description": "The next appearance opens from a slim center seam.",
"categories": [
"actions"
],
"dependencies": [
"lucide-react",
"motion"
],
"registryDependencies": [
"https://manicat.viraza.net/r/manicat-foundation.json",
"https://manicat.viraza.net/r/button.json"
],
"files": [
{
"path": "registry/components/theme-switch/theme-switch.tsx",
"type": "registry:component",
"target": "@components/manicat/theme-switch/theme-switch.tsx"
},
{
"path": "registry/components/theme-switch/theme-switch.module.css",
"type": "registry:component",
"target": "@components/manicat/theme-switch/theme-switch.module.css"
},
{
"path": "registry/components/button/button.tsx",
"type": "registry:component",
"target": "@components/manicat/button/button.tsx"
},
{
"path": "registry/components/button/button.module.css",
"type": "registry:component",
"target": "@components/manicat/button/button.module.css"
}
],
"docs": "Docs and live preview: https://uiarc.dev/components/theme-switch-split",
"meta": {
"tier": "free",
"kind": "component",
"docs": "https://manicat.viraza.net/components/theme-switch-split",
"markdown": "https://uiarc.dev/components/theme-switch-split/markdown",
"tags": [
"theme",
"appearance",
"motion"
]
}
}Usage
Install the component, then copy this into your app. Change the content and the props to fit your screen.
example.tsxtsx
"use client";
import { useEffect, useState } from "react";
import { ThemeSwitch } from "@/components/manicat/theme-switch/theme-switch";
import { useTheme } from "fumadocs-ui/provider/base";
function useMounted() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
const frame = requestAnimationFrame(() => setMounted(true));
return () => cancelAnimationFrame(frame);
}, []);
return mounted;
}
export function ThemeSwitchSplitExample() {
const { resolvedTheme, setTheme } = useTheme();
const mounted = useMounted();
if (!mounted) return null;
return (
<ThemeSwitch
variant="split"
theme={resolvedTheme === "dark" ? "dark" : "light"}
onThemeChange={(next) => setTheme(next)}
/>
);
}
Examples
Real screens rather than isolated props. Switch to Code to copy a whole example into your project.
Split A split-circle toggle for compact toolbars.
Split
A split-circle toggle for compact toolbars. preview
Live · keyboard ready
split-a-split-circle-toggle-for-compact-toolbars-.tsxtsx
"use client";
import { useState } from "react";
import { ThemeSwitch } from "@/components/manicat/theme-switch/theme-switch";
export function ThemeSwitchSplitHeaderExample() {
const [theme, setTheme] = useState<"light" | "dark">("light");
return <ThemeSwitch variant="split" theme={theme} onThemeChange={setTheme} />;
}
Keyboard interactions
| Keys | Action |
|---|---|
| Enter / Space | Toggles the theme. |
Accessibility
- Same button as theme-switch: aria-pressed reflects dark mode and the label names the next theme.
- Keep the split reveal off for reduced motion and swap the theme directly.
Motion
- variant="split": the next appearance opens from a slim center seam; run it in onThemeChange with a view transition.
- The icon shifts 1px right in dark mode.
- Reduced motion removes the nudge and swaps the icon with a fade.
Responsive behavior
- The labelled button fits a desktop top bar; pass iconOnly for a fixed 38px square on narrow headers and mobile.
- Hover styles apply only on hover-capable fine pointers, so touch taps do not leave a stuck hover state.
Performance
- The button itself is tiny: one icon swap on a spring, and the first theme after hydration applies without animating.
- The page transition is yours; a full-page view transition snapshots the whole document, so keep it short and skip it for reduced motion.
Notes for AI
- Select with variant="split". The prop only styles the icon nudge and is passed back to onThemeChange; the page animation is your code.
- Split is symmetric and does not depend on where the button sits, so it suits centered layouts.