Skip to content

Theme Toggle Sun Moon Morph

A track+thumb switch whose icon morphs between a sun and a crescent moon.

Freeplayfulminimal
Category
Components
Added
2026-08-24
Deps
1
Updated
2026-09-01
Preview

Props

This entry takes no configurable props.

Requires framer-motion
"use client";

import { useState } from "react";
import { motion } from "framer-motion";

export function ThemeToggleSunMoonMorph({
  onChange,
}: {
  onChange?: (dark: boolean) => void;
}) {
  const [dark, setDark] = useState(false);

  function toggle() {
    const next = !dark;
    setDark(next);
    onChange?.(next);
  }

  return (
    <button
      onClick={toggle}
      role="switch"
      aria-checked={dark}
      className={`relative flex h-9 w-16 items-center rounded-full transition-colors ${dark ? "bg-neutral-900" : "bg-neutral-200"}`}
    >
      <motion.div
        className="absolute top-1 left-1 flex h-7 w-7 items-center justify-center rounded-full bg-white shadow-md"
        animate={{ x: dark ? 28 : 0 }}
        transition={{ type: "spring", stiffness: 400, damping: 30 }}
      >
        <svg viewBox="0 0 24 24" width="16" height="16">
          <circle cx="12" cy="12" r="6" fill={dark ? "#161616" : "#ff9d2d"} />
          {[0, 45, 90, 135, 180, 225, 270, 315].map((deg) => (
            <motion.line
              key={deg}
              x1="12"
              y1="2"
              x2="12"
              y2="4.5"
              stroke="#ff9d2d"
              strokeWidth="2"
              strokeLinecap="round"
              style={{ transformOrigin: "12px 12px", rotate: deg }}
              animate={{ opacity: dark ? 0 : 1, scale: dark ? 0 : 1 }}
              transition={{ duration: 0.25 }}
            />
          ))}
          <motion.circle
            cy="8"
            r="7"
            fill="#ffffff"
            initial={{ cx: 22 }}
            animate={{ opacity: dark ? 1 : 0, cx: dark ? 16 : 22 }}
            transition={{ duration: 0.3 }}
          />
        </svg>
      </motion.div>
    </button>
  );
}
Notes

About this component

Most dark-mode toggles swap two separate icon components and call it a transition; this one animates a single set of SVG primitives — a center circle, eight rays, and a second bite circle — so the sun visibly reconfigures into a crescent moon rather than one icon replacing another. Reach for this over an icon-swap toggle when that continuity is worth the extra SVG complexity. It shipped with a real bug worth knowing before extending it: the bite circle's position attribute was originally set as a plain SVG string while also being driven by Framer's animate prop, and without a matching initial value to interpolate from, the browser threw a runtime error on mount. The fix is to always pair animate with an explicit initial on any SVG geometry attribute Framer takes over, never leave one as a bare attribute string. Separately, the bite circle has to be filled white to match the thumb it visually sits on top of, not the page background behind the thumb. One caveat: the sun and moon colors are hardcoded hex values, not the site's design tokens, so a brand reskin won't reach this component.

Curator’s note

Caught two real bugs while building this: (1) the crescent 'bite' circle was initially filled with the page canvas color to mask the sun, but the bite sits on top of the THUMB (a white circle), not directly on the canvas — fixed by filling it white, matching what it actually sits on. (2) that same circle's `cx` was set as a plain string SVG attribute (cx="17") while also being animated via Framer's `animate` prop — Framer needs a matching `initial` value to interpolate from when a motion value takes over an attribute; without it the browser threw 'attribute cx: Expected length, "undefined"' on mount. Fixed by moving the starting value into an explicit `initial={{ cx: 22 }}` and dropping the plain attribute.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

A checkbox whose checkmark draws itself in as a real SVG path animation.

checkboxformsvg
NewPro

A segmented control where only the selected option expands to its label.

segmentedtogglemorph

A three-line menu icon that morphs smoothly into an X.

iconmenuhamburger