CheckBox

Animated checkbox that draws the checkmark smoothly when toggled

Last updated on

Edit on GitHub

Manual

Install the following dependencies:

npm install react-native-reanimated react-native-svg

Copy and paste the following code into your project. component/organisms/check-box

import React, { memo, useEffect, useRef, useState } from "react";import Animated, {  Easing,  interpolate,  useAnimatedProps,  useSharedValue,  withSpring,  withTiming,} from "react-native-reanimated";import {  G,  Path,  Svg,  // @ts-check  type PathProps,  type GProps,} from "react-native-svg";import type { ICheckbox, IStrokePath } from "./types";import { BOX_PATH, PADDING, TICK_PATH, VIEWPORT_SIZE } from "./conf";const AnimatedSvgPath = Animated.createAnimatedComponent<PathProps>(Path);const AnimatedG = Animated.createAnimatedComponent<GProps>(G);const StrokePath: React.FC<IStrokePath> = ({  animValue,  ...pathProps}: IStrokePath): React.ReactNode & React.JSX.Element => {  const [pathLength, setPathLength] = useState<number>(0);  const pathRef = useRef<typeof AnimatedSvgPath>(null);  const animatedStrokeProps = useAnimatedProps<    Pick<PathProps, "strokeDashoffset">  >(() => {    const easedProgress = Easing.bezierFn(0.37, 0, 0.63, 1)(animValue.value);    const offset = pathLength - pathLength * easedProgress;    return {      strokeDashoffset: Math.max(0, offset),    };  });  const handleLayout = () => {    if (pathRef.current) {      // @ts-ignore      const totalLength = pathRef.current?.getTotalLength();      setPathLength(totalLength);    }  };  return (    <AnimatedSvgPath      animatedProps={animatedStrokeProps}      fill="none"      onLayout={handleLayout}      // @ts-ignore      ref={pathRef}      strokeDasharray={pathLength}      {...pathProps}    />  );};export const Checkbox: React.FC<ICheckbox> = memo<ICheckbox>(  ({ checked = false, checkmarkColor, stroke = 1.5, size }: ICheckbox) => {    const animValue = useSharedValue<number>(0);    const scaleValue = useSharedValue<number>(1);    useEffect(() => {      animValue.value = withTiming<number>(checked ? 1 : 0, {        duration: checked ? 300 : 250,        easing: checked          ? Easing.bezier(0.4, 0, 0.2, 1)          : Easing.bezier(0.4, 0, 0.6, 1),      });      if (checked) {        scaleValue.value = withSpring<number>(1, {          damping: 10,          stiffness: 150,          mass: 0.5,        });      } else {        scaleValue.value = withTiming<number>(1, { duration: 100 });      }    }, [checked, animValue, scaleValue]);    const animatedCheckmarkProps = useAnimatedProps<Pick<GProps, "transform">>(      () => {        const scale = interpolate(scaleValue.value, [0, 1], [0.8, 1]);        return {          transform: [            { translateX: 32 },            { translateY: 32 },            { scale },            { translateX: -32 },            { translateY: -32 },          ],        };      },    );    const viewBox = [      -PADDING,      -PADDING,      VIEWPORT_SIZE + PADDING,      VIEWPORT_SIZE + PADDING,    ].join(" ");    return (      <Svg        viewBox={viewBox}        style={{          transform: [            {              scale: size ? size / VIEWPORT_SIZE : 1,            },          ],        }}      >        {/* <Defs>          <ClipPath id="clipPath">            <Path              d={BOX_PATH}              fill="white"              stroke="gray"              strokeLinecap="round"              strokeLinejoin="round"            />          </ClipPath>        </Defs> */}        <G clipPath="url(#clipPath)">          <AnimatedG animatedProps={animatedCheckmarkProps}>            <StrokePath              animValue={animValue}              d={TICK_PATH}              stroke={checkmarkColor}              strokeWidth={stroke}              strokeLinecap="round"              strokeLinejoin="round"            />          </AnimatedG>        </G>      </Svg>    );  },);export default memo<React.FunctionComponent<ICheckbox>>(Checkbox);

Usage

import { View, Text, Pressable, StyleSheet } from "react-native";import { GestureHandlerRootView } from "react-native-gesture-handler";import { StatusBar } from "expo-status-bar";import { useState } from "react";import { useFonts } from "expo-font";import CheckBox from "@/components/organisms/check-box";export default function App() {  const [checked, setChecked] = useState(false);  const [fontLoaded] = useFonts({    SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"),    HelveticaNowDisplay: require("@/assets/fonts/HelveticaNowDisplayMedium.ttf"),    StretchPro: require("@/assets/fonts/StretchPro.otf"),  });  return (    <GestureHandlerRootView style={styles.container}>      <StatusBar style="light" />      <View style={{ marginTop: 100 }}>        <Pressable onPress={() => setChecked(!checked)} style={styles.card}>          <View style={styles.left}>            <Text              style={[                styles.title,                {                  fontFamily: fontLoaded ? "HelveticaNowDisplay" : undefined,                },              ]}            >              Love Reacticx?            </Text>            <Text              style={[                styles.subtitle,                {                  fontFamily: fontLoaded ? "SfProRounded" : undefined,                },              ]}            >              Tap to toggle            </Text>          </View>          <View style={styles.checkbox}>            <CheckBox              checked={checked}              checkmarkColor="#fff"              stroke={5.5}              size={60}            />          </View>        </Pressable>      </View>    </GestureHandlerRootView>  );}const styles = StyleSheet.create({  container: {    flex: 1,    backgroundColor: "#000",    // justifyContent: "center",    paddingHorizontal: 24,  },  card: {    flexDirection: "row",    alignItems: "center",    justifyContent: "space-between",    padding: 20,    borderRadius: 18,    backgroundColor: "rgba(255,255,255,0.08)",  },  left: {    gap: 2,  },  title: {    color: "#fff",    fontSize: 17,    fontWeight: "600",  },  subtitle: {    color: "rgba(255,255,255,0.6)",    fontSize: 13,  },  checkbox: {    width: 44,    height: 44,    borderRadius: 12,    backgroundColor: "rgba(255,255,255,0.12)",    justifyContent: "center",    alignItems: "center",  },});

Props

React Native Reanimated
React Native Svg