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(Path);const AnimatedG = Animated.createAnimatedComponent(G);const StrokePath: React.FC<IStrokePath> = ({  animValue,  ...pathProps}: IStrokePath): React.ReactNode & React.JSX.Element => {  const [pathLength, setPathLength] = useState(0);  const pathRef = useRef(null);  const animatedStrokeProps = useAnimatedProps<    Pick<PathProps, "strokeDashoffset" | "opacity">  >(() => {    if (pathLength === 0) {      return {        strokeDashoffset: 1,        opacity: 0,      };    }    const easedProgress = Easing.bezierFn(0.37, 0, 0.63, 1)(animValue.value);    const offset = pathLength - pathLength * easedProgress;    return {      strokeDashoffset: Math.max(0, offset),      opacity: 1,    };  });  const handleLayout = () => {    if (pathRef.current) {      // @ts-ignore      const totalLength = pathRef.current?.getTotalLength();      setPathLength(totalLength);    }  };  return (    <AnimatedSvgPath      ref={pathRef}      onLayout={handleLayout}      strokeDasharray={pathLength}      animatedProps={animatedStrokeProps}      {...pathProps}    />  );};export const Checkbox: React.FC<ICheckbox> = memo(  ({    checked = false,    checkmarkColor,    stroke = 1.5,    size,    showBorder = false,  }: ICheckbox) => {    const animValue = useSharedValue(checked ? 1 : 0);    const borderAnimValue = useSharedValue(showBorder ? 1 : 0);    const scaleValue = useSharedValue(1);    const isFirstRender = useRef(true);    useEffect(() => {      if (isFirstRender.current) {        isFirstRender.current = false;        animValue.value = checked ? 1 : 0;        borderAnimValue.value = showBorder ? 1 : 0;        scaleValue.value = 1;        return;      }      animValue.value = withTiming(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(1, {          damping: 10,          stiffness: 150,          mass: 0.5,        });      } else {        scaleValue.value = withTiming(1, { duration: 100 });      }    }, [checked, animValue, scaleValue]);    useEffect(() => {      if (isFirstRender.current) return;      borderAnimValue.value = withTiming(showBorder ? 1 : 0, {        duration: 250,        easing: showBorder          ? Easing.bezier(0.4, 0, 0.2, 1)          : Easing.bezier(0.4, 0, 0.6, 1),      });    }, [showBorder, borderAnimValue]);    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 width={size} height={size} viewBox={viewBox}>        <StrokePath          d={BOX_PATH}          stroke={checkmarkColor}          strokeWidth={stroke}          fill="none"          strokeLinecap="round"          strokeLinejoin="round"          animValue={borderAnimValue}        />        <AnimatedG animatedProps={animatedCheckmarkProps}>          <StrokePath            d={TICK_PATH}            stroke={checkmarkColor}            strokeWidth={stroke}            fill="none"            strokeLinecap="round"            strokeLinejoin="round"            animValue={animValue}          />        </AnimatedG>      </Svg>    );  },);export default memo<React.FC<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

On this page