import { useEffect, useId } from "react"; import { useFirstMountState, 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, theme } from "../../lib/styles/stitches.config"; const Button = styled("button", { border: 0, padding: "0.6em", marginRight: "-0.6em", background: "none", cursor: "pointer", color: theme.colors.mediumDark, "&:hover": { color: theme.colors.warning, }, }); export type ThemeToggleProps = { className?: string; }; const ThemeToggle = ({ className }: ThemeToggleProps) => { const hasMounted = useHasMounted(); const { activeTheme, setTheme } = useTheme(); const isFirstMount = useFirstMountState(); const prefersReducedMotion = useMedia("(prefers-reduced-motion: reduce)", false); const maskId = useId(); // SSR-safe ID to cross-reference areas of the SVG // default to light since `activeTheme` might be undefined const safeTheme = activeTheme === "dark" ? activeTheme : "light"; // accessibility: disable animation if user prefers reduced motion useEffect(() => { Globals.assign({ skipAnimation: !!isFirstMount || !!prefersReducedMotion, }); }, [isFirstMount, 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 ThemeToggle;