import { useEffect, memo } from "react"; import { useMedia } from "react-use"; import { useSpring, animated, Globals } from "@react-spring/web"; import { useTheme } from "../../hooks/use-theme"; import { useHasMounted } from "../../hooks/use-has-mounted"; import { styled } from "../../lib/styles/stitches.config"; const Button = styled("button", { border: 0, padding: "0.6em", marginRight: "-0.6em", background: "none", cursor: "pointer", color: "$mediumDark", "&:hover": { color: "$warning", }, }); export type ThemeToggleProps = { id?: string; className?: string; }; const ThemeToggle = ({ id = "nav", className }: ThemeToggleProps) => { const hasMounted = useHasMounted(); const prefersReducedMotion = useMedia("(prefers-reduced-motion: reduce)", false); const { resolvedTheme, setTheme } = useTheme(); // default to light since `resolvedTheme` might be undefined const safeTheme = resolvedTheme === "dark" ? "dark" : "light"; // accessibility: skip animation if user prefers reduced motion useEffect(() => { Globals.assign({ skipAnimation: !!prefersReducedMotion, }); }, [prefersReducedMotion]); // modified from https://jfelix.info/blog/using-react-spring-to-animate-svg-icons-dark-mode-toggle const springProperties = { light: { svg: { transform: "rotate(90deg)", }, circle: { r: 5, }, mask: { cx: "100%", cy: "0%", }, lines: { opacity: 1, }, }, dark: { svg: { transform: "rotate(40deg)", }, circle: { r: 9, }, mask: { cx: "50%", cy: "23%", }, lines: { opacity: 0, }, }, springConfig: { mass: 4, tension: 250, friction: 35 }, }; const { svg, circle, mask, lines } = springProperties[safeTheme]; const svgContainerProps = useSpring({ ...svg, config: springProperties.springConfig, }); const centerCircleProps = useSpring({ ...circle, config: springProperties.springConfig, }); const maskedCircleProps = useSpring({ ...mask, config: springProperties.springConfig, }); const linesProps = useSpring({ ...lines, config: springProperties.springConfig, }); // render a blank div of the same size to avoid layout shifting until we're fully mounted and self-aware if (!hasMounted) { return ( ); } return ( ); }; export default memo(ThemeToggle);