Animated Theme Toggle
A smooth animated theme toggle button made with React Native Svg
Last updated on
Manual
Install the following dependencies:
npm install react-native-reanimated react-native-svgCopy and paste the following code into your project.
component/micro-interactions/animated-theme-toggle.tsx
// @ts-checkimport React, { memo } from "react";import { Pressable } from "react-native";import Animated, { useAnimatedProps, useSharedValue, withTiming, interpolate, Easing, useDerivedValue,} from "react-native-reanimated";import Svg, { Path, PathProps } from "react-native-svg";import type { IAnimatedThemeToggle } from "./types";import { MOON_PATH, PATH_LENGTHS, SUN_PATHS } from "./const";const AnimatedPath = Animated.createAnimatedComponent<PathProps>(Path);export const AnimatedThemeToggle: React.FC<IAnimatedThemeToggle> & React.FunctionComponent<IAnimatedThemeToggle> = memo<IAnimatedThemeToggle>( ({ isDark, onToggle, size = 24, duration = 700, color = "#000000", strokeWidth = 2, style, }) => { const progress = useSharedValue<number>(isDark ? 1 : 0); React.useEffect(() => { progress.value = withTiming<number>(isDark ? 1 : 0, { duration, easing: Easing.inOut(Easing.ease), }); }, [isDark, duration]); const scaleSun = useDerivedValue<number>(() => interpolate(progress.value, [0, 1], [1, 0]), ); const scaleMoon = useDerivedValue<number>(() => interpolate(progress.value, [0, 1], [0, 1]), ); const pathLengthSun = useDerivedValue<number>(() => interpolate(scaleSun.value, [0, 0.6, 1], [0, 0, 1]), ); const pathLengthMoon = useDerivedValue<number>(() => interpolate(scaleMoon.value, [0, 0.6, 1], [0, 0, 1]), ); const sunCirclePropz = useAnimatedProps< Pick<PathProps, "strokeDasharray" | "transform"> >(() => { const drawn = pathLengthSun.value * PATH_LENGTHS.sunCircle; return { strokeDasharray: `${drawn} ${PATH_LENGTHS.sunCircle}`, transform: [ { translateX: 12.5 * (1 - scaleSun.value) }, { translateY: 12.5 * (1 - scaleSun.value) }, { scale: scaleSun.value }, ], }; }); const sunRayPropz = useAnimatedProps< Pick<PathProps, "strokeDasharray" | "transform"> >(() => { const drawn = pathLengthSun.value * PATH_LENGTHS.sunRay; return { strokeDasharray: `${drawn} ${PATH_LENGTHS.sunRay}`, transform: [ { translateX: 12.5 * (1 - scaleSun.value) }, { translateY: 12.5 * (1 - scaleSun.value) }, { scale: scaleSun.value }, ], }; }); const moonPropz = useAnimatedProps< Pick<PathProps, "strokeDasharray" | "transform"> >(() => { const drawn = pathLengthMoon.value * PATH_LENGTHS.moon; return { strokeDasharray: `${drawn} ${PATH_LENGTHS.moon}`, transform: [ { translateX: 12.5 * (1 - scaleMoon.value) }, { translateY: 12.5 * (1 - scaleMoon.value) }, { scale: scaleMoon.value }, ], }; }); return ( <Pressable onPress={onToggle} style={[style]}> <Svg width={size} height={size} viewBox="0 0 25 25" fill="none"> <AnimatedPath d={SUN_PATHS.circle} stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" animatedProps={sunCirclePropz} /> {SUN_PATHS.rays.map<React.JSX.Element>( (d: string, index?: number) => ( <AnimatedPath key={`sun-ray-${index}`} d={d} stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" animatedProps={sunRayPropz} /> ), )} <AnimatedPath d={MOON_PATH} stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" animatedProps={moonPropz} /> </Svg> </Pressable> ); },);Usage
import { View, Text, StyleSheet } from "react-native";import { GestureHandlerRootView } from "react-native-gesture-handler";import { StatusBar } from "expo-status-bar";import { useFonts } from "expo-font";import { useState } from "react";import { AnimatedThemeToggle } from "@/components/micro-interactions/animated-theme-toggle";export default function App() { const [fontLoaded] = useFonts({ SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"), }); const [isDark, setIsDark] = useState(false); return ( <GestureHandlerRootView style={styles.container}> <StatusBar style="light" animated /> <View style={styles.row}> <AnimatedThemeToggle isDark={isDark} onToggle={() => setIsDark(!isDark)} size={98} color="#fff" strokeWidth={1.3} /> </View> </GestureHandlerRootView> );}const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#0a0a0a", alignItems: "center", }, row: { flexDirection: "row", alignItems: "center", gap: 12, top: 120, }, label: { fontSize: 16, color: "#fff", width: 40, },});Props
React Native Reanimated
React Native Svg
