Button

A customize button with press animation

Last updated on

Edit on GitHub

Manual

Install the following dependencies:

npm install react-native-reanimated react-native-worklets expo-linear-gradient

Copy and paste the following code into your project. component/base/button.tsx

// @ts-checkimport React, { memo, useEffect } from "react";import {  Pressable,  StyleSheet,  Platform,  View,  type ViewStyle,  ActivityIndicator,} from "react-native";import Animated, {  useSharedValue,  useAnimatedStyle,  withTiming,  interpolate,  Easing,  interpolateColor,} from "react-native-reanimated";import { LinearGradient } from "expo-linear-gradient";// @ts-checkimport type { IButton } from "./types";export const Button: React.FC<IButton> & React.FunctionComponent<IButton> =  memo<IButton>(    ({      children,      isLoading = false,      onPress,      width = 200,      height = 48,      backgroundColor = "#fff",      loadingText = "Loading...",      loadingTextColor = "white",      loadingTextSize = 16,      borderRadius,      gradientColors,      style,      loadingTextStyle,      withPressAnimation = true,      animationDuration = 250,      disabled = false,      showLoadingIndicator = false,      renderLoadingIndicator,      loadingTextBackgroundColor = "#cacaca",    }: IButton): React.ReactNode & React.JSX.Element & React.ReactElement => {      const animationProgress = useSharedValue<number>(isLoading ? 1 : 0);      const scaleValue = useSharedValue<number>(1);      useEffect(() => {        animationProgress.value = withTiming<number>(isLoading ? 1 : 0, {          duration: animationDuration,          easing: Easing.bezier(0.4, 0, 0.2, 1),        });      }, [isLoading, animationDuration]);      const calculatedBorderRadius = borderRadius ?? height / 2;      const contentAnimatedStylez = useAnimatedStyle<        Pick<ViewStyle, "transform" | "opacity">      >(() => {        const translateY = interpolate(          animationProgress.value,          [0, 1],          [0, -20],        );        const opacity = interpolate(animationProgress.value, [0, 0.5], [1, 0]);        return {          transform: [{ translateY }],          opacity,        };      });      const loadingAnimatedStylez = useAnimatedStyle<        Pick<ViewStyle, "transform" | "opacity">      >(() => {        const translateY = interpolate(          animationProgress.value,          [0, 1],          [20, 0],        );        const opacity = interpolate(animationProgress.value, [0.5, 1], [0, 1]);        return {          transform: [{ translateY }],          opacity,        };      });      const pressAnimatedStylez = useAnimatedStyle<        Pick<ViewStyle, "transform" | "backgroundColor">      >(() => {        const bgColor = interpolateColor(          animationProgress.value,          [0, 1],          [backgroundColor, loadingTextBackgroundColor!],        );        return {          transform: [{ scale: scaleValue.value }],          backgroundColor: bgColor,        };      });      const handlePressIn = () => {        if (withPressAnimation && !disabled && !isLoading) {          scaleValue.value = withTiming(0.95, { duration: 100 });        }      };      const handlePressOut = () => {        if (withPressAnimation && !disabled && !isLoading) {          scaleValue.value = withTiming(1, { duration: 200 });        }      };      const renderInnerContent = () => (        <View style={styles.contentWrapper}>          <Animated.View            style={[styles.contentContainer, contentAnimatedStylez]}          >            {children}          </Animated.View>          <Animated.View            style={[styles.loadingContainer, loadingAnimatedStylez]}          >            {showLoadingIndicator &&              (renderLoadingIndicator ? (                renderLoadingIndicator()              ) : (                <Animated.View style={{ marginRight: loadingText ? 8 : 0 }}>                  <ActivityIndicator color={"#000"} size={"small"} />                </Animated.View>              ))}            <Animated.Text              style={[                styles.loadingText,                {                  color: loadingTextColor,                  fontSize: loadingTextSize,                },                loadingTextStyle,              ]}            >              {loadingText}            </Animated.Text>          </Animated.View>        </View>      );      const buttonContent = gradientColors ? (        <Animated.View style={[pressAnimatedStylez]}>          <LinearGradient            colors={gradientColors as [string, string, ...string[]]}            start={{ x: 0, y: 0 }}            end={{ x: 1, y: 0 }}            style={[              styles.button,              {                width,                height,                borderRadius: calculatedBorderRadius,              },              style,            ]}          >            {renderInnerContent()}          </LinearGradient>        </Animated.View>      ) : (        <Animated.View          style={[            styles.button,            {              width,              height,              backgroundColor,              borderRadius: calculatedBorderRadius,            },            pressAnimatedStylez,            style,          ]}        >          {renderInnerContent()}        </Animated.View>      );      return (        <Pressable          onPress={onPress}          disabled={isLoading || disabled}          onPressIn={handlePressIn}          onPressOut={handlePressOut}          style={({ pressed }) => [            styles.pressable,            Platform.OS === "ios" && pressed && styles.pressed,          ]}          accessible={true}          accessibilityRole="button"          accessibilityState={{ disabled: isLoading || disabled }}        >          {buttonContent}        </Pressable>      );    },  );const styles = StyleSheet.create({  pressable: {    alignSelf: "flex-start",  },  pressed: {    opacity: 0.9,  },  button: {    justifyContent: "center",    alignItems: "center",    overflow: "hidden",  },  contentWrapper: {    position: "relative",    justifyContent: "center",    alignItems: "center",    width: "100%",    height: "100%",  },  contentContainer: {    position: "absolute",    flexDirection: "row",    alignItems: "center",    justifyContent: "center",  },  loadingContainer: {    position: "absolute",    flexDirection: "row",    alignItems: "center",    justifyContent: "center",  },  loadingText: {    fontWeight: "600",  },});export default memo<React.FC<IButton> & React.FunctionComponent<IButton>>(  Button,);

Usage

import { View, StyleSheet, Text } from "react-native";import { GestureHandlerRootView } from "react-native-gesture-handler";import { StatusBar } from "expo-status-bar";import { useFonts } from "expo-font";import { Ionicons } from "@expo/vector-icons";import { useCallback, useState } from "react";import { Button } from "@/components/base/button";import { CircularLoader } from "@/components/molecules/Loaders/circular";export default function App() {  const [fontLoaded] = useFonts({    SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"),  });  const [loading, setLoading] = useState<boolean>(false);  const onPress = useCallback(() => {    setLoading(true);    setTimeout(() => {      setLoading(false);    }, 3000);  }, []);  return (    <GestureHandlerRootView style={styles.container}>      <StatusBar style="light" />      <Button        loadingText="Fetching…"        isLoading={loading}        onPress={onPress}        loadingTextColor="#000"        showLoadingIndicator        loadingTextStyle={{          fontFamily: fontLoaded ? "SfProRounded" : undefined,        }}        renderLoadingIndicator={() => (          <View style={{ marginRight: 8 }}>            <CircularLoader              size={18}              strokeWidth={2.5}              enableBlur              gradientLength={50}              duration={500}            />          </View>        )}      >        <View style={styles.btn}>          <Ionicons name="arrow-forward" size={18} color="black" />          <Text            style={[              styles.btnText,              {                fontFamily: fontLoaded ? "SfProRounded" : undefined,              },            ]}          >            Click Me!          </Text>        </View>      </Button>    </GestureHandlerRootView>  );}const styles = StyleSheet.create({  container: {    flex: 1,    backgroundColor: "#000",    alignItems: "center",    paddingTop: 110,  },  btn: {    flexDirection: "row",    alignItems: "center",    gap: 10,    backgroundColor: "#fff",    paddingVertical: 16,    paddingHorizontal: 32,    borderRadius: 16,  },  btnText: {    fontSize: 17,    fontWeight: "600",    color: "#000",  },});

Props

React Native Reanimated