46 / 47
All 47 in 47 Tailwind CSS Toggles & Switches
- 01Tailwind iOS-Style Toggle Switch with appearance-none
- 02Tailwind Dark Mode Sun / Moon Theme Switcher
- 03Tailwind Neumorphic Soft-UI Pill Toggle
- 04Tailwind Gooey Liquid Morphing Switch with SVG Filter
- 05Tailwind Bouncy Elastic Physics Toggle (Overshoot Cubic-Bezier)
- 06Tailwind ON / OFF Text Switch with Labels Inside the Track
- 07Tailwind SaaS Pricing Toggle — Monthly / Annually Segmented Pill
- 08Tailwind Mechanical Keycap Toggles with Haptic Feedback
- 09Tailwind Waveform Mute Toggle with Animated EQ Bars
- 10Tailwind Timed Auto-Off Toggle with Conic Countdown Ring
- 11Tailwind Press-and-Hold to Confirm Toggle (Fingerprint Scan)
- 12Tailwind Split-Flap Departure Board Toggle (3D Flip)
- 13Tailwind Permission Toggle Group with :has() Live Status Badge
- 14Tailwind Material Design 3 Switch with State Layer
- 15Tailwind Skeuomorphic Wall Light Switch with Room Relight
- 16Tailwind Neon Cyberpunk Toggle with Clipped Corners
- 17Tailwind Brutalist Snap Toggle with Hard Offset Shadow
- 18Tailwind Day / Night Landscape Scene Switch
- 19Tailwind 3D Flip Card Toggle (rotateY Switch)
- 20Tailwind Sliding Pill Toggle with Label Inside the Thumb
- 21Tailwind Glassmorphism Frosted Toggle with backdrop-blur
- 22Tailwind Rotary Knob Toggle with Brushed-Metal Conic Gradient
- 23Tailwind iOS Settings List with Grouped Toggle Rows
- 24Tailwind Minimal Outline Toggle Drawn in currentColor
- 25Tailwind Squash-and-Stretch Pebble Toggle
- 26Tailwind Industrial Rocker I/O Power Switch
- 27Tailwind Accessible role=switch Toggle with forced-colors Support
- 28Tailwind Macro-Pad Toggle Grid with RGB Underglow
- 29Tailwind Radial VU Meter Mute Toggle
- 30Tailwind Segmented Pip Countdown Auto-Off Toggle
- 31Tailwind Slide-to-Confirm Toggle Rail (Drag or Arrow Keys)
- 32Tailwind Odometer Roll ON / OFF Toggle
- 33Tailwind Master Toggle with Indeterminate Tri-State
- 34Tailwind iOS Control Centre Tile Toggles
- 35Tailwind Material 3 Connected Icon Toggle Buttons
- 36Tailwind Two-Gang Wall Plate with Cumulative Room Light
- 37Tailwind Neon Toggle with Animated Conic Border Light
- 38Tailwind Brutalist Rubber-Stamp Toggle
- 39Tailwind Aurora Night Sky Toggle Switch
- 40Tailwind 3D Rotating Cube Toggle
- 41Tailwind Three-Position Off / Auto / On Selector
- 42Tailwind Liquid Glass Toggle with Specular Highlight
- 43Tailwind Three-Detent Rotary Selector Knob
- 44Tailwind Hardware Slide Switch with Knurled Grip
- 45Tailwind Battery Saver Toggle with Live Fill Meter
- 46Tailwind Morphing Sun-to-Moon Icon Toggle (SVG Stroke)
- 47Tailwind Guarded Kill Switch Under a Flip-Up Safety Cover
Tailwind Morphing Sun-to-Moon Icon Toggle (SVG Stroke)
One SVG becomes two icons: the sun's rays retract into their own centres via stroke-dashoffset while a mask circle slides in to bite a crescent out of the disc. No icon swap, no second file — the same eight paths morph.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="ttg-46 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg46-bg font-ttg46-sans transition-colors duration-500 has-checked:bg-ttg46-nightbg motion-reduce:transition-none" aria-label="Sun to moon icon toggle">
<div class="flex flex-col items-center gap-6">
<input type="checkbox" id="ttg-46-sw" role="switch" aria-label="Dark theme" class="peer sr-only">
<label for="ttg-46-sw" class="grid place-items-center w-24 h-24 rounded-full cursor-pointer bg-ttg46-btn border border-ttg46-edge transition-colors duration-500 peer-checked:bg-ttg46-btnnight peer-checked:border-ttg46-edgenight peer-focus-visible:outline-3 peer-focus-visible:outline-ttg46-focus peer-focus-visible:outline-offset-4 peer-checked:[&_.ttg46-icon]:-rotate-[25deg] peer-checked:[&_.ttg46-ray]:[stroke-dashoffset:9px] peer-checked:[&_.ttg46-bite]:translate-x-0 peer-checked:[&_.ttg46-disc]:fill-ttg46-moon">
<svg viewBox="0 0 48 48" class="ttg46-icon w-12 h-12 transition-transform duration-500 ease-[cubic-bezier(0.3,1,0.35,1.05)] motion-reduce:duration-75" aria-hidden="true">
<defs>
<mask id="ttg46-crescent">
<rect x="0" y="0" width="48" height="48" fill="white"/>
<circle class="ttg46-bite transition-transform duration-500 ease-[cubic-bezier(0.3,1,0.35,1.05)] translate-x-[26px] motion-reduce:duration-75" cx="30" cy="18" r="13" fill="black"/>
</mask>
</defs>
<circle class="ttg46-disc fill-ttg46-sun transition-[fill] duration-500" cx="24" cy="24" r="10" mask="url(#ttg46-crescent)"/>
<g class="ttg46-rays" stroke="currentColor" stroke-width="3" stroke-linecap="round" fill="none">
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:0ms]" x1="24" y1="3" x2="24" y2="10"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:40ms]" x1="38.8" y1="9.2" x2="34" y2="14"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:80ms]" x1="45" y1="24" x2="38" y2="24"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:120ms]" x1="38.8" y1="38.8" x2="34" y2="34"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:160ms]" x1="24" y1="45" x2="24" y2="38"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:200ms]" x1="9.2" y1="38.8" x2="14" y2="34"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:240ms]" x1="3" y1="24" x2="10" y2="24"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:280ms]" x1="9.2" y1="9.2" x2="14" y2="14"/>
</g>
</svg>
</label>
<span class="flex flex-col items-center gap-1 text-center">
<b class="text-[15.5px] font-semibold text-ttg46-ink transition-colors duration-500 peer-checked:text-ttg46-inknight">Dark theme</b>
<span class="text-[12.5px] text-ttg46-mute transition-colors duration-500">One icon, morphed — not two icons swapped</span>
</span>
</div>
</section><section class="ttg-46 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg46-bg font-ttg46-sans transition-colors duration-500 has-checked:bg-ttg46-nightbg motion-reduce:transition-none" aria-label="Sun to moon icon toggle">
<div class="flex flex-col items-center gap-6">
<input type="checkbox" id="ttg-46-sw" role="switch" aria-label="Dark theme" class="peer sr-only">
<label for="ttg-46-sw" class="grid place-items-center w-24 h-24 rounded-full cursor-pointer bg-ttg46-btn border border-ttg46-edge transition-colors duration-500 peer-checked:bg-ttg46-btnnight peer-checked:border-ttg46-edgenight peer-focus-visible:outline-3 peer-focus-visible:outline-ttg46-focus peer-focus-visible:outline-offset-4 peer-checked:[&_.ttg46-icon]:-rotate-[25deg] peer-checked:[&_.ttg46-ray]:[stroke-dashoffset:9px] peer-checked:[&_.ttg46-bite]:translate-x-0 peer-checked:[&_.ttg46-disc]:fill-ttg46-moon">
<svg viewBox="0 0 48 48" class="ttg46-icon w-12 h-12 transition-transform duration-500 ease-[cubic-bezier(0.3,1,0.35,1.05)] motion-reduce:duration-75" aria-hidden="true">
<defs>
<mask id="ttg46-crescent">
<rect x="0" y="0" width="48" height="48" fill="white"/>
<circle class="ttg46-bite transition-transform duration-500 ease-[cubic-bezier(0.3,1,0.35,1.05)] translate-x-[26px] motion-reduce:duration-75" cx="30" cy="18" r="13" fill="black"/>
</mask>
</defs>
<circle class="ttg46-disc fill-ttg46-sun transition-[fill] duration-500" cx="24" cy="24" r="10" mask="url(#ttg46-crescent)"/>
<g class="ttg46-rays" stroke="currentColor" stroke-width="3" stroke-linecap="round" fill="none">
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:0ms]" x1="24" y1="3" x2="24" y2="10"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:40ms]" x1="38.8" y1="9.2" x2="34" y2="14"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:80ms]" x1="45" y1="24" x2="38" y2="24"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:120ms]" x1="38.8" y1="38.8" x2="34" y2="34"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:160ms]" x1="24" y1="45" x2="24" y2="38"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:200ms]" x1="9.2" y1="38.8" x2="14" y2="34"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:240ms]" x1="3" y1="24" x2="10" y2="24"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:280ms]" x1="9.2" y1="9.2" x2="14" y2="14"/>
</g>
</svg>
</label>
<span class="flex flex-col items-center gap-1 text-center">
<b class="text-[15.5px] font-semibold text-ttg46-ink transition-colors duration-500 peer-checked:text-ttg46-inknight">Dark theme</b>
<span class="text-[12.5px] text-ttg46-mute transition-colors duration-500">One icon, morphed — not two icons swapped</span>
</span>
</div>
</section>@import "tailwindcss";
@theme {
--color-ttg46-bg: #f3f4f7;
--color-ttg46-bg: oklch(0.96 0.005 265);
--color-ttg46-nightbg: #0d1120;
--color-ttg46-nightbg: oklch(0.18 0.03 270);
--color-ttg46-btn: #ffffff;
--color-ttg46-btn: oklch(1 0 0);
--color-ttg46-btnnight: #191f33;
--color-ttg46-btnnight: oklch(0.24 0.03 270);
--color-ttg46-edge: #e5e7ee;
--color-ttg46-edge: oklch(0.92 0.006 265);
--color-ttg46-edgenight: #2a3350;
--color-ttg46-edgenight: oklch(0.32 0.04 270);
--color-ttg46-sun: #e8a317;
--color-ttg46-sun: oklch(0.75 0.16 78);
--color-ttg46-moon: #e6ecff;
--color-ttg46-moon: oklch(0.93 0.03 270);
--color-ttg46-ink: #222838;
--color-ttg46-ink: oklch(0.27 0.018 265);
--color-ttg46-inknight: #dfe4f2;
--color-ttg46-inknight: oklch(0.91 0.015 270);
--color-ttg46-mute: #7d8494;
--color-ttg46-mute: oklch(0.6 0.018 265);
--font-ttg46-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-46 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-ttg46-bg: #f3f4f7;
--color-ttg46-bg: oklch(0.96 0.005 265);
--color-ttg46-nightbg: #0d1120;
--color-ttg46-nightbg: oklch(0.18 0.03 270);
--color-ttg46-btn: #ffffff;
--color-ttg46-btn: oklch(1 0 0);
--color-ttg46-btnnight: #191f33;
--color-ttg46-btnnight: oklch(0.24 0.03 270);
--color-ttg46-edge: #e5e7ee;
--color-ttg46-edge: oklch(0.92 0.006 265);
--color-ttg46-edgenight: #2a3350;
--color-ttg46-edgenight: oklch(0.32 0.04 270);
--color-ttg46-sun: #e8a317;
--color-ttg46-sun: oklch(0.75 0.16 78);
--color-ttg46-moon: #e6ecff;
--color-ttg46-moon: oklch(0.93 0.03 270);
--color-ttg46-ink: #222838;
--color-ttg46-ink: oklch(0.27 0.018 265);
--color-ttg46-inknight: #dfe4f2;
--color-ttg46-inknight: oklch(0.91 0.015 270);
--color-ttg46-mute: #7d8494;
--color-ttg46-mute: oklch(0.6 0.018 265);
--font-ttg46-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-46 {
width: 100%;
min-height: 100svh;
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-ttg46-bg: #f3f4f7;
--color-ttg46-bg: oklch(0.96 0.005 265);
--color-ttg46-nightbg: #0d1120;
--color-ttg46-nightbg: oklch(0.18 0.03 270);
--color-ttg46-btn: #ffffff;
--color-ttg46-btn: oklch(1 0 0);
--color-ttg46-btnnight: #191f33;
--color-ttg46-btnnight: oklch(0.24 0.03 270);
--color-ttg46-edge: #e5e7ee;
--color-ttg46-edge: oklch(0.92 0.006 265);
--color-ttg46-edgenight: #2a3350;
--color-ttg46-edgenight: oklch(0.32 0.04 270);
--color-ttg46-sun: #e8a317;
--color-ttg46-sun: oklch(0.75 0.16 78);
--color-ttg46-moon: #e6ecff;
--color-ttg46-moon: oklch(0.93 0.03 270);
--color-ttg46-ink: #222838;
--color-ttg46-ink: oklch(0.27 0.018 265);
--color-ttg46-inknight: #dfe4f2;
--color-ttg46-inknight: oklch(0.91 0.015 270);
--color-ttg46-mute: #7d8494;
--color-ttg46-mute: oklch(0.6 0.018 265);
--font-ttg46-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-46 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-ttg46-bg: #f3f4f7;
--color-ttg46-bg: oklch(0.96 0.005 265);
--color-ttg46-nightbg: #0d1120;
--color-ttg46-nightbg: oklch(0.18 0.03 270);
--color-ttg46-btn: #ffffff;
--color-ttg46-btn: oklch(1 0 0);
--color-ttg46-btnnight: #191f33;
--color-ttg46-btnnight: oklch(0.24 0.03 270);
--color-ttg46-edge: #e5e7ee;
--color-ttg46-edge: oklch(0.92 0.006 265);
--color-ttg46-edgenight: #2a3350;
--color-ttg46-edgenight: oklch(0.32 0.04 270);
--color-ttg46-sun: #e8a317;
--color-ttg46-sun: oklch(0.75 0.16 78);
--color-ttg46-moon: #e6ecff;
--color-ttg46-moon: oklch(0.93 0.03 270);
--color-ttg46-ink: #222838;
--color-ttg46-ink: oklch(0.27 0.018 265);
--color-ttg46-inknight: #dfe4f2;
--color-ttg46-inknight: oklch(0.91 0.015 270);
--color-ttg46-mute: #7d8494;
--color-ttg46-mute: oklch(0.6 0.018 265);
--font-ttg46-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-46 {
width: 100%;
min-height: 100svh;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The rays' resting
[stroke-dashoffset:0]is a UTILITY, not a scoped rule — and that is load-bearing. Tailwind emits utilities inside@layer utilities, and any unlayered CSS beats a layered rule regardless of specificity, so a plain.ttg46-ray { stroke-dashoffset: 0 }would silently outrank thepeer-checked:variant and freeze the sun. Base states that a variant must override belong in the same layer as the variant. - Eight rays, eight
[transition-delay:Nms]utilities written straight into the class list. A stagger of eight is the boundary where inline delays stay more readable than a loop — past that, generate the markup. - The crescent is an SVG
<mask>with a translated circle, and the mask id is namespacedttg46-crescentbecause SVG ids are document-global — the same discipline as the gooey filter in demo 04. peer-checked:[&_.ttg46-disc]:fill-ttg46-moonworks because Tailwind'sfill-*utilities apply to SVG elements like any other; the icon needs no inline styling at all.- New in this set: a theme toggle whose icon is one object rather than a crossfade between two — and the reduced-motion path shortens every transition to 75ms instead of removing them, so the morph still reads as a change of state.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Morphing Sun-to-Moon Icon Toggle (SVG Stroke)
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-morphing-sun-to-moon-icon-toggle/
One SVG becomes two icons: the sun's rays retract into their own centres via stroke-dashoffset while a mask circle slides in to bite a crescent out of the disc. No icon swap, no second file — the same eight paths morph.
## HTML
```html
<section class="ttg-46 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg46-bg font-ttg46-sans transition-colors duration-500 has-checked:bg-ttg46-nightbg motion-reduce:transition-none" aria-label="Sun to moon icon toggle">
<div class="flex flex-col items-center gap-6">
<input type="checkbox" id="ttg-46-sw" role="switch" aria-label="Dark theme" class="peer sr-only">
<label for="ttg-46-sw" class="grid place-items-center w-24 h-24 rounded-full cursor-pointer bg-ttg46-btn border border-ttg46-edge transition-colors duration-500 peer-checked:bg-ttg46-btnnight peer-checked:border-ttg46-edgenight peer-focus-visible:outline-3 peer-focus-visible:outline-ttg46-focus peer-focus-visible:outline-offset-4 peer-checked:[&_.ttg46-icon]:-rotate-[25deg] peer-checked:[&_.ttg46-ray]:[stroke-dashoffset:9px] peer-checked:[&_.ttg46-bite]:translate-x-0 peer-checked:[&_.ttg46-disc]:fill-ttg46-moon">
<svg viewBox="0 0 48 48" class="ttg46-icon w-12 h-12 transition-transform duration-500 ease-[cubic-bezier(0.3,1,0.35,1.05)] motion-reduce:duration-75" aria-hidden="true">
<defs>
<mask id="ttg46-crescent">
<rect x="0" y="0" width="48" height="48" fill="white"/>
<circle class="ttg46-bite transition-transform duration-500 ease-[cubic-bezier(0.3,1,0.35,1.05)] translate-x-[26px] motion-reduce:duration-75" cx="30" cy="18" r="13" fill="black"/>
</mask>
</defs>
<circle class="ttg46-disc fill-ttg46-sun transition-[fill] duration-500" cx="24" cy="24" r="10" mask="url(#ttg46-crescent)"/>
<g class="ttg46-rays" stroke="currentColor" stroke-width="3" stroke-linecap="round" fill="none">
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:0ms]" x1="24" y1="3" x2="24" y2="10"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:40ms]" x1="38.8" y1="9.2" x2="34" y2="14"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:80ms]" x1="45" y1="24" x2="38" y2="24"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:120ms]" x1="38.8" y1="38.8" x2="34" y2="34"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:160ms]" x1="24" y1="45" x2="24" y2="38"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:200ms]" x1="9.2" y1="38.8" x2="14" y2="34"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:240ms]" x1="3" y1="24" x2="10" y2="24"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:280ms]" x1="9.2" y1="9.2" x2="14" y2="14"/>
</g>
</svg>
</label>
<span class="flex flex-col items-center gap-1 text-center">
<b class="text-[15.5px] font-semibold text-ttg46-ink transition-colors duration-500 peer-checked:text-ttg46-inknight">Dark theme</b>
<span class="text-[12.5px] text-ttg46-mute transition-colors duration-500">One icon, morphed — not two icons swapped</span>
</span>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg46-bg: #f3f4f7;
--color-ttg46-bg: oklch(0.96 0.005 265);
--color-ttg46-nightbg: #0d1120;
--color-ttg46-nightbg: oklch(0.18 0.03 270);
--color-ttg46-btn: #ffffff;
--color-ttg46-btn: oklch(1 0 0);
--color-ttg46-btnnight: #191f33;
--color-ttg46-btnnight: oklch(0.24 0.03 270);
--color-ttg46-edge: #e5e7ee;
--color-ttg46-edge: oklch(0.92 0.006 265);
--color-ttg46-edgenight: #2a3350;
--color-ttg46-edgenight: oklch(0.32 0.04 270);
--color-ttg46-sun: #e8a317;
--color-ttg46-sun: oklch(0.75 0.16 78);
--color-ttg46-moon: #e6ecff;
--color-ttg46-moon: oklch(0.93 0.03 270);
--color-ttg46-ink: #222838;
--color-ttg46-ink: oklch(0.27 0.018 265);
--color-ttg46-inknight: #dfe4f2;
--color-ttg46-inknight: oklch(0.91 0.015 270);
--color-ttg46-mute: #7d8494;
--color-ttg46-mute: oklch(0.6 0.018 265);
--font-ttg46-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-46 {
width: 100%;
min-height: 100svh;
}
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Morphing Sun-to-Moon Icon Toggle (SVG Stroke)
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-morphing-sun-to-moon-icon-toggle/
One SVG becomes two icons: the sun's rays retract into their own centres via stroke-dashoffset while a mask circle slides in to bite a crescent out of the disc. No icon swap, no second file — the same eight paths morph.
## HTML
```html
<section class="ttg-46 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg46-bg font-ttg46-sans transition-colors duration-500 has-checked:bg-ttg46-nightbg motion-reduce:transition-none" aria-label="Sun to moon icon toggle">
<div class="flex flex-col items-center gap-6">
<input type="checkbox" id="ttg-46-sw" role="switch" aria-label="Dark theme" class="peer sr-only">
<label for="ttg-46-sw" class="grid place-items-center w-24 h-24 rounded-full cursor-pointer bg-ttg46-btn border border-ttg46-edge transition-colors duration-500 peer-checked:bg-ttg46-btnnight peer-checked:border-ttg46-edgenight peer-focus-visible:outline-3 peer-focus-visible:outline-ttg46-focus peer-focus-visible:outline-offset-4 peer-checked:[&_.ttg46-icon]:-rotate-[25deg] peer-checked:[&_.ttg46-ray]:[stroke-dashoffset:9px] peer-checked:[&_.ttg46-bite]:translate-x-0 peer-checked:[&_.ttg46-disc]:fill-ttg46-moon">
<svg viewBox="0 0 48 48" class="ttg46-icon w-12 h-12 transition-transform duration-500 ease-[cubic-bezier(0.3,1,0.35,1.05)] motion-reduce:duration-75" aria-hidden="true">
<defs>
<mask id="ttg46-crescent">
<rect x="0" y="0" width="48" height="48" fill="white"/>
<circle class="ttg46-bite transition-transform duration-500 ease-[cubic-bezier(0.3,1,0.35,1.05)] translate-x-[26px] motion-reduce:duration-75" cx="30" cy="18" r="13" fill="black"/>
</mask>
</defs>
<circle class="ttg46-disc fill-ttg46-sun transition-[fill] duration-500" cx="24" cy="24" r="10" mask="url(#ttg46-crescent)"/>
<g class="ttg46-rays" stroke="currentColor" stroke-width="3" stroke-linecap="round" fill="none">
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:0ms]" x1="24" y1="3" x2="24" y2="10"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:40ms]" x1="38.8" y1="9.2" x2="34" y2="14"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:80ms]" x1="45" y1="24" x2="38" y2="24"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:120ms]" x1="38.8" y1="38.8" x2="34" y2="34"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:160ms]" x1="24" y1="45" x2="24" y2="38"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:200ms]" x1="9.2" y1="38.8" x2="14" y2="34"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:240ms]" x1="3" y1="24" x2="10" y2="24"/>
<line class="ttg46-ray stroke-ttg46-sun [stroke-dasharray:9px] [stroke-dashoffset:0px] transition-[stroke-dashoffset,stroke] duration-300 [transition-delay:280ms]" x1="9.2" y1="9.2" x2="14" y2="14"/>
</g>
</svg>
</label>
<span class="flex flex-col items-center gap-1 text-center">
<b class="text-[15.5px] font-semibold text-ttg46-ink transition-colors duration-500 peer-checked:text-ttg46-inknight">Dark theme</b>
<span class="text-[12.5px] text-ttg46-mute transition-colors duration-500">One icon, morphed — not two icons swapped</span>
</span>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg46-bg: #f3f4f7;
--color-ttg46-bg: oklch(0.96 0.005 265);
--color-ttg46-nightbg: #0d1120;
--color-ttg46-nightbg: oklch(0.18 0.03 270);
--color-ttg46-btn: #ffffff;
--color-ttg46-btn: oklch(1 0 0);
--color-ttg46-btnnight: #191f33;
--color-ttg46-btnnight: oklch(0.24 0.03 270);
--color-ttg46-edge: #e5e7ee;
--color-ttg46-edge: oklch(0.92 0.006 265);
--color-ttg46-edgenight: #2a3350;
--color-ttg46-edgenight: oklch(0.32 0.04 270);
--color-ttg46-sun: #e8a317;
--color-ttg46-sun: oklch(0.75 0.16 78);
--color-ttg46-moon: #e6ecff;
--color-ttg46-moon: oklch(0.93 0.03 270);
--color-ttg46-ink: #222838;
--color-ttg46-ink: oklch(0.27 0.018 265);
--color-ttg46-inknight: #dfe4f2;
--color-ttg46-inknight: oklch(0.91 0.015 270);
--color-ttg46-mute: #7d8494;
--color-ttg46-mute: oklch(0.6 0.018 265);
--font-ttg46-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-46 {
width: 100%;
min-height: 100svh;
}
```How this works
Rays retract, they do not fade. Each ray is a short line with stroke-dasharray equal to its own length; animating stroke-dashoffset to that same value draws it back into itself. Eight rays with staggered delays make the sun collapse from the outside in.
The crescent is a mask, not a second path. A circle inside an SVG <mask> starts off-canvas and slides over the disc when checked, subtracting a bite. That is how you get a true crescent whose edge is a perfect arc.
The rotation ties it together. Without the −25° turn the two states look like two icons; with it, one object transformed.
Make it yours
- Stagger direction: reverse the delays to make the rays retract inward-out.
- Faster morph: drop to 260ms and halve the stagger — it stays readable.
- Different pair: a cloud/sun or lock/unlock morph works with the same dash technique.
- Use it as a real theme switch by moving
has-checked:up to the document element.
Gotchas — read before shipping
stroke-dasharraymust match each ray's actual length, or the retraction leaves a stub or finishes early.- Dash values need UNITS in CSS:
stroke-dashoffset: 9is legal as an SVG presentation attribute but invalid as a CSS declaration, so the browser drops it and the rays never retract. Always write9pxin a utility. - Never set a variant's base value in unlayered CSS — unlayered declarations win over everything in
@layer utilities, so the variant compiles correctly and still does nothing. - Give every mask a namespaced id — SVG ids are document-global and a second copy of the demo would silently reuse the first mask.
vector-effect: non-scaling-strokeif you scale the icon, or the strokes thicken with it.- Under
motion-reduce:the icon must still change: the rays snap and the crescent jumps, rather than nothing happening.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
SVG masks and stroke-dash animation are universally supported; the demo has no feature-query fallback because there is nothing to fall back from.