How to make an animated heart like button in React Native

By the LumioUI team · Updated

An animated like button is a heart that fills when tapped, gives a small pulse and a haptic tick, and updates the like count in place. In React Native, keep liked in state, animate the fill and scale with Reanimated, and update the server optimistically so it feels instant. The LumioUI Like Button fills from the bottom up with a soft glow, and fills the same way when someone double-taps a photo.

Live demo of the LumioUI Like Button component. It's interactive, so try it.

How the heart animation works

  1. Two layers. An outline heart with a filled heart on top. Animate the fill's opacity, or for a nicer effect a clip that reveals it from the bottom up.
  2. A short pulse on like only. Press in slightly, then spring back. Don't pulse on unlike, which should stay quiet.
  3. Haptics. A light selection tick confirms the tap without a sound.
  4. Optimistic count. Change the number before the request returns, and roll back if it fails. Animate only the digits that change.
  5. Double-tap to like. A double tap on the photo should set the same state and play the same fill, never toggle it off.
  6. Accessibility. A 44pt touch target (use hitSlop), accessibilityState.selected, and a label that names the action.
TSX Heart.tsx
import * as React from "react";
import { Pressable, Text } from "react-native";
import * as Haptics from "expo-haptics";
import Animated, { useAnimatedStyle, useSharedValue, withSequence, withSpring, withTiming } from "react-native-reanimated";

export function Heart({ liked, onChange, count }) {
  const scale = useSharedValue(1);
  const fill = useSharedValue(liked ? 1 : 0);

  const press = () => {
    const next = !liked;
    onChange(next);
    Haptics.selectionAsync();
    fill.value = withTiming(next ? 1 : 0, { duration: 220 });
    if (next) scale.value = withSequence(withTiming(0.85, { duration: 80 }), withSpring(1, { damping: 8, stiffness: 300 }));
  };

  const heart = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] }));
  const inner = useAnimatedStyle(() => ({ opacity: fill.value }));

  return (
    <Pressable onPress={press} hitSlop={10}
      accessibilityRole="button" accessibilityLabel="Like" accessibilityState={{ selected: liked }}>
      <Animated.View style={heart}>
        <Text>♡</Text>
        <Animated.Text style={[{ position: "absolute", color: "#ff3b5c" }, inner]}>♥</Animated.Text>
      </Animated.View>
      <Text>{count}</Text>
    </Pressable>
  );
}

The quick way: LumioUI Like Button

The Like Button fills from the bottom up with a soft glow, updates the count in place, ticks a haptic and respects reduced motion. It's free and runs in Expo Go.

TSX PostActions.tsx
import * as React from "react";
import { LikeButton } from "@/components/ui/like-button/like-button";

// post.likes is your store's count; update it with the same optimistic change.
export function PostActions({ post }) {
  const [liked, setLiked] = React.useState(post.likedByMe);
  return (
    <LikeButton
      liked={liked}
      onLikedChange={(next) => {
        setLiked(next);
        api.setLiked(post.id, next); // optimistic: the UI already changed
      }}
      count={post.likes}
      variant="plain"
      size="md"
    />
  );
}
  • liked and onLikedChange make it controlled. Use defaultLiked to let it manage itself.
  • count with formatCount shortens big numbers (1.2K). hideCount hides the number.
  • variant is "plain", "pill" or "overlay" (for photos), and size is "sm", "md" or "lg". noBurst and haptic tune the feedback.
  • For bookmarks use Save Button. For emoji reactions use Reaction Picker (Pro).

Add it to your app

LumioUI's CLI copies the component's source into your project, so it's yours to read and change. Run init once per project:

npx @lumioui/cli init
npx @lumioui/cli add like-button

It works in Expo (SDK 54+) and bare React Native, with Reanimated 4 and NativeWind. See the installation guide and the full Like Button API.

More React Native guides