Split

The next appearance opens from a slim center seam.

themeappearancemotion
Preview
Split preview

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

Keyboard interactions

KeysAction
Enter / SpaceToggles 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.