Scale Animated Button

A reusable React Native button component that provides a subtle scaling animation on press. Built with Reanimated, it supports loading/disabled states, icons, and respects reduced motion settings.

reanimatedbuttonanimationreact-nativescale

Installation

ScaleAnimatedButton.tsx
import { ReactElement } from "react";
import { ActivityIndicator, Pressable, StyleSheet, Text } from "react-native";
import Animated, {
  interpolate,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from "react-native-reanimated";

export type ResizingButtonProps = {
  accessibilityHint?: string;
  accessibilityLabel?: string;
  Icon?: ReactElement;
  isDisabled?: boolean;
  isLoading?: boolean;
  onPress: () => void;
  buttonColor: string;
  textColor: string;
  scale?: number;
  title: string;
  reduceMotion?: "never" | "always" | "system";
}

const DURATION = 300;


export const ScaleAnimatedButton = ({
  accessibilityHint,
  accessibilityLabel,
  Icon,
  isDisabled = false,
  isLoading = false,
  onPress,
  buttonColor,
  textColor,
  scale = 0.95,
  title,
  reduceMotion = "system",
}: ResizingButtonProps) => {
  const transition = useSharedValue(0);
  const isActive = useSharedValue(false);

  const motion =
  	reduceMotion === "never"
  		? ReduceMotion.Never
  		: reduceMotion === "always"
  			? ReduceMotion.Always
  			: ReduceMotion.System;

  const animatedStyle = useAnimatedStyle(() => ({
  	transform: [
  		{
  			scale: interpolate(transition.value, [0, 1], [1, scale]),
  		},
  	],
  }));

  return (
  	<Pressable
  		accessibilityHint={accessibilityHint}
  		accessibilityLabel={accessibilityLabel}
  		accessibilityRole="button"
  		accessibilityState={{
  			busy: isLoading,
  			disabled: isDisabled || isLoading,
  		}}
  		disabled={isDisabled || isLoading}
  		
  		onPress={onPress}
  		onPressIn={() => {
  			isActive.value = true;
  			transition.value = withTiming(1, { duration: DURATION, reduceMotion: motion, }, () => {
  				if (!isActive.value) {
  					transition.value = withTiming(0, {
  						duration: DURATION,
  						reduceMotion: motion,
  					});
  				}
  			});
  		}}
  		onPressOut={() => {
  			if (transition.value === 1) {
  				transition.value = withTiming(0, {
  					duration: DURATION,
  					reduceMotion: motion,
  				});
  			}
  			isActive.value = false;
  		}}
  	>
  		<Animated.View
  			style={[
  				styles.container,
  				animatedStyle,
  				{
  					opacity: isDisabled ? 0.5 : 1,
  					backgroundColor: buttonColor,
  				},
  			]}
  		>
  			{isLoading ? (
  				<ActivityIndicator color={textColor} size={18} />
  			) : (
  				<>
  					{Icon}
  					<Text
  						numberOfLines={1}
  						style={[styles.title, { color: textColor }]}
  					>
  						{title}
  					</Text>
  				</>
  			)}
  		</Animated.View>
  	</Pressable>
  );
};

const styles = StyleSheet.create({
  container: {
  	alignItems: "center",
  	borderRadius: 8,
  	flexDirection: "row",
  	gap: 8,
  	height: 42,
  	justifyContent: "center",
  	paddingHorizontal: 12,
  	paddingVertical: 8,
  },
  title: {
  	flexShrink: 1,
  	fontSize: 18,
  	fontWeight: "600",
  },
});

Usage

App.tsx
import React, { useState } from 'react';
import { View, StyleSheet, Alert } from 'react-native';
import { ScaleAnimatedButton } from "@/components/ui/Buttons/ScaleAnimatedButton";
import MaterialIcons from '@expo/vector-icons/MaterialIcons';

export default function App() {
const [isLoading, setIsLoading] = useState(false);

const handleLoadingPress = () => {
  setIsLoading(true);
  setTimeout(() => {
    setIsLoading(false);
  }, 2500); 
};

return (
  <ScaleAnimatedButton
  	buttonColor={colors.AuxColorTwo}
  	textColor={colors.Neutral0}
  	onPress={() => {}}
  	title={"Scale Action"}
  	reduceMotion="never"
  	Icon={<MaterialIcons name="payment" size={18} color={colors.Neutral0} />}
  />
);}

Props

PropTypeDefaultRequiredDescription
titlestringYesText label displayed on the button.
onPress() => voidYesFunction called when the button is pressed.
IconReactElementNoOptional icon element displayed left of the title.
isDisabledbooleanfalseNoDisables button interaction and styles it.
isLoadingbooleanfalseNoShows loading indicator instead of title/icon.
scalenumber0.95NoScale factor applied during press animation.
reduceMotion'never' | 'always' | 'system''system'NoControls if animation respects system motion settings.
accessibilityHintstringNoAccessibility hint for screen readers.
accessibilityLabelstringNoMain accessibility label for the button.