Created
May 6, 2026 15:45
-
-
Save TomIsLoading/4bbbddd349eb68ff7252b86f4e305717 to your computer and use it in GitHub Desktop.
Fancy animated dark mode toggle with React, TailwindCSS & Framer Motion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Dispatch, SetStateAction, useState } from "react"; | |
| import { motion } from "framer-motion"; | |
| import { BsFillCloudyFill, BsStarFill } from "react-icons/bs"; | |
| const ToggleWrapper = () => { | |
| const [mode, setMode] = useState<"dark" | "light">("dark"); | |
| return ( | |
| <div | |
| className={`absolute inset-0 z-100 flex items-center justify-center transition-colors ${ | |
| mode === "dark" ? "bg-slate-900" : "bg-slate-50" | |
| }`} | |
| > | |
| <DarkModeToggle mode={mode} setMode={setMode} /> | |
| </div> | |
| ); | |
| }; | |
| const DarkModeToggle = ({ | |
| mode, | |
| setMode, | |
| }: { | |
| mode: "light" | "dark"; | |
| setMode: Dispatch<SetStateAction<"dark" | "light">>; | |
| }) => { | |
| return ( | |
| <button | |
| onClick={() => setMode(mode === "dark" ? "light" : "dark")} | |
| role="switch" | |
| className={`p-2 w-28 rounded-full flex shadow-lg relative bg-linear-to-b ${ | |
| mode === "light" | |
| ? "justify-end from-blue-500 to-sky-300" | |
| : "justify-start from-indigo-600 to-indigo-400" | |
| } `} | |
| > | |
| <Thumb mode={mode} /> | |
| {mode === "light" && <Clouds />} | |
| {mode === "dark" && <Stars />} | |
| </button> | |
| ); | |
| }; | |
| const Thumb = ({ mode }: { mode: "light" | "dark" }) => { | |
| return ( | |
| <motion.div | |
| layout | |
| transition={{ | |
| type: "spring", | |
| stiffness: 420, | |
| damping: 32, | |
| mass: 0.6, | |
| }} | |
| className={`h-10 w-10 rounded-full overflow-hidden shadow-lg relative`} | |
| > | |
| <div | |
| className={`absolute inset-0 ${ | |
| mode === "dark" | |
| ? "bg-slate-100" | |
| : "animate-pulse bg-linear-to-tr from-amber-300 to-yellow-500 rounded-full" | |
| }`} | |
| /> | |
| {mode === "light" && <SunCenter />} | |
| {mode === "dark" && <MoonSpots />} | |
| </motion.div> | |
| ); | |
| }; | |
| const SunCenter = () => ( | |
| <div className="absolute inset-1 rounded-full bg-amber-300" /> | |
| ); | |
| const MoonSpots = () => ( | |
| <> | |
| <motion.div | |
| initial={{ x: -4, opacity: 0 }} | |
| animate={{ x: 0, opacity: 1 }} | |
| transition={{ delay: 0.15, duration: 0.35 }} | |
| className="w-3 h-3 rounded-full bg-slate-300 absolute right-2.5 bottom-1" | |
| /> | |
| <motion.div | |
| initial={{ x: -4, opacity: 0 }} | |
| animate={{ x: 0, opacity: 1 }} | |
| transition={{ delay: 0.2, duration: 0.35 }} | |
| className="w-3 h-3 rounded-full bg-slate-300 absolute left-1 bottom-4" | |
| /> | |
| <motion.div | |
| initial={{ x: -4, opacity: 0 }} | |
| animate={{ x: 0, opacity: 1 }} | |
| transition={{ delay: 0.25, duration: 0.35 }} | |
| className="w-2 h-2 rounded-full bg-slate-300 absolute right-2 top-2" | |
| /> | |
| </> | |
| ); | |
| const Clouds = () => ( | |
| <> | |
| <motion.span | |
| animate={{ x: [-20, -15, -10, -5, 0], opacity: [0, 1, 0.75, 1, 0] }} | |
| transition={{ | |
| duration: 10, | |
| repeat: Infinity, | |
| delay: 0.25, | |
| }} | |
| className="text-white text-xs absolute left-10 top-1" | |
| > | |
| <BsFillCloudyFill /> | |
| </motion.span> | |
| <motion.span | |
| animate={{ x: [-10, 0, 10, 20, 30], opacity: [0, 1, 0.75, 1, 0] }} | |
| transition={{ | |
| duration: 20, | |
| repeat: Infinity, | |
| delay: 0.5, | |
| }} | |
| className="text-white text-lg absolute left-4 top-4" | |
| > | |
| <BsFillCloudyFill /> | |
| </motion.span> | |
| <motion.span | |
| animate={{ x: [-7, 0, 7, 14, 21], opacity: [0, 1, 0.75, 1, 0] }} | |
| transition={{ | |
| duration: 12.5, | |
| repeat: Infinity, | |
| }} | |
| className="text-white absolute left-9 top-8" | |
| > | |
| <BsFillCloudyFill /> | |
| </motion.span> | |
| <motion.span | |
| animate={{ x: [-15, 0, 15, 30, 45], opacity: [0, 1, 0.75, 1, 0] }} | |
| transition={{ | |
| duration: 25, | |
| repeat: Infinity, | |
| delay: 0.75, | |
| }} | |
| className="text-white absolute text-xs left-14 top-4" | |
| > | |
| <BsFillCloudyFill /> | |
| </motion.span> | |
| </> | |
| ); | |
| const Stars = () => ( | |
| <> | |
| <motion.span | |
| animate={{ | |
| scale: [0.75, 1, 0.75], | |
| opacity: [0.75, 1, 0.75], | |
| }} | |
| transition={{ | |
| repeat: Infinity, | |
| duration: 5, | |
| ease: "easeIn", | |
| }} | |
| className="text-slate-300 text-xs absolute right-10 top-2" | |
| > | |
| <BsStarFill /> | |
| </motion.span> | |
| <motion.span | |
| animate={{ | |
| scale: [1, 0.75, 1], | |
| opacity: [0.5, 0.25, 0.5], | |
| }} | |
| transition={{ | |
| repeat: Infinity, | |
| duration: 3.5, | |
| ease: "easeIn", | |
| }} | |
| style={{ rotate: "-45deg" }} | |
| className="text-slate-300 text-lg absolute right-4 top-3" | |
| > | |
| <BsStarFill /> | |
| </motion.span> | |
| <motion.span | |
| animate={{ | |
| scale: [1, 0.5, 1], | |
| opacity: [1, 0.5, 1], | |
| }} | |
| style={{ rotate: "45deg" }} | |
| transition={{ | |
| repeat: Infinity, | |
| duration: 2.5, | |
| ease: "easeIn", | |
| }} | |
| className="text-slate-300 absolute right-8 top-8" | |
| > | |
| <BsStarFill /> | |
| </motion.span> | |
| </> | |
| ); | |
| export default ToggleWrapper; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment