logoChisa UI

Command Palette

Search for a command to run...

Componentschevron_rightFavoriteButton

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 }}>
<FavoriteButton
onToggle={(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';
5
6export 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);
15
16 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]);
27
28 const handlePress = () => {
29 const newValue = !isChecked;
30 if (!isControlled) setInternalChecked(newValue);
31 onToggle?.(newValue);
32 };
33
34 const rOutlineStyle = useAnimatedStyle(() => ({
35 opacity: interpolate(progress.value, [0, 0.2], [1, 0]),
36 transform: [{ scale: scaleHeart.value }]
37 }));
38
39 const rFilledStyle = useAnimatedStyle(() => ({
40 opacity: interpolate(progress.value, [0, 0.2], [0, 1]),
41 transform: [{ scale: scaleHeart.value }]
42 }));
43
44 const rTextStyle = useAnimatedStyle(() => ({
45 transform: [{ translateY: interpolate(progress.value, [0, 1], [0, -20]) }]
46 }));
47
48 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]) }));
50
51 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};
68
69const 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

NameTypeDefaultDescription
checkedbooleanfalseControlled state
onToggle(checked: boolean) => void-Toggle callback
labelAddstring'Add to Favorites'Label when unchecked
labelAddedstring'Added to Favorites'Label when checked