FavoriteButton
Heart switch button with pulse animation and sliding text labels.
9:41
signal_cellular_altwifibattery_full
Installation
$
Usage
Example.tsx
import { FavoriteButton } from './components/FavoriteButton';export default function App() {return (<View style={{ padding: 20 }}><FavoriteButtononToggle={(checked) => console.log('Favorited:', checked)}/></View>);}
Full Component Code
FavoriteButton.tsx
1import React, { useState, useEffect } from 'react';2import { StyleSheet, Text, View, Pressable, ViewStyle } from 'react-native';3import Animated, { useSharedValue, useAnimatedStyle, withTiming, withSequence, interpolate } from 'react-native-reanimated';4import { Ionicons } from '@expo/vector-icons';56export const FavoriteButton = ({7 initialChecked = false, checked: controlledChecked, onToggle, style,8 labelAdd = 'Add to Favorites', labelAdded = 'Added to Favorites',9}) => {10 const isControlled = controlledChecked !== undefined;11 const [internalChecked, setInternalChecked] = useState(initialChecked);12 const isChecked = isControlled ? controlledChecked : internalChecked;13 const progress = useSharedValue(isChecked ? 1 : 0);14 const scaleHeart = useSharedValue(1);1516 useEffect(() => {17 progress.value = withTiming(isChecked ? 1 : 0, { duration: 300 });18 if (isChecked) {19 scaleHeart.value = withSequence(20 withTiming(1.3, { duration: 250 }), withTiming(1, { duration: 250 }),21 withTiming(1.3, { duration: 250 }), withTiming(1, { duration: 250 })22 );23 } else {24 scaleHeart.value = withTiming(1, { duration: 200 });25 }26 }, [isChecked]);2728 const handlePress = () => {29 const newValue = !isChecked;30 if (!isControlled) setInternalChecked(newValue);31 onToggle?.(newValue);32 };3334 const rOutlineStyle = useAnimatedStyle(() => ({35 opacity: interpolate(progress.value, [0, 0.2], [1, 0]),36 transform: [{ scale: scaleHeart.value }]37 }));3839 const rFilledStyle = useAnimatedStyle(() => ({40 opacity: interpolate(progress.value, [0, 0.2], [0, 1]),41 transform: [{ scale: scaleHeart.value }]42 }));4344 const rTextStyle = useAnimatedStyle(() => ({45 transform: [{ translateY: interpolate(progress.value, [0, 1], [0, -20]) }]46 }));4748 const rTextOneStyle = useAnimatedStyle(() => ({ opacity: interpolate(progress.value, [0, 0.5], [1, 0]) }));49 const rTextTwoStyle = useAnimatedStyle(() => ({ opacity: interpolate(progress.value, [0.5, 1], [0, 1]) }));5051 return (52 <Pressable onPress={handlePress} style={[styles.container, style]}>53 <View style={styles.content}>54 <View style={styles.iconContainer}>55 <Animated.View style={[styles.iconLayer, rOutlineStyle]}><Ionicons name="heart-outline" size={24} color="#000" /></Animated.View>56 <Animated.View style={[styles.iconLayer, rFilledStyle]}><Ionicons name="heart" size={24} color="#ef4444" /></Animated.View>57 </View>58 <View style={styles.textWrapper}>59 <Animated.View style={[styles.textSlider, rTextStyle]}>60 <Animated.View style={[styles.textContainer, rTextOneStyle]}><Text style={styles.text}>{labelAdd}</Text></Animated.View>61 <Animated.View style={[styles.textContainer, rTextTwoStyle]}><Text style={styles.text}>{labelAdded}</Text></Animated.View>62 </Animated.View>63 </View>64 </View>65 </Pressable>66 );67};6869const styles = StyleSheet.create({70 container: { backgroundColor: 'white', padding: 10, paddingRight: 15, borderRadius: 15, shadowColor: '#959da5', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.2, shadowRadius: 24, elevation: 8, alignSelf: 'flex-start' },71 content: { flexDirection: 'row', alignItems: 'center', gap: 14 },72 iconContainer: { width: 24, height: 24, justifyContent: 'center', alignItems: 'center' },73 iconLayer: { position: 'absolute' },74 textWrapper: { height: 20, overflow: 'hidden', justifyContent: 'flex-start' },75 textSlider: { flexDirection: 'column' },76 textContainer: { height: 20, justifyContent: 'center' },77 text: { fontSize: 15, color: 'black', fontWeight: '500' },78});
Props
| Name | Type | Default | Description |
|---|---|---|---|
| checked | boolean | false | Controlled state |
| onToggle | (checked: boolean) => void | - | Toggle callback |
| labelAdd | string | 'Add to Favorites' | Label when unchecked |
| labelAdded | string | 'Added to Favorites' | Label when checked |
