# Tinder swipe card animation in React Native with Reanimated

> By Lawrence Arya, Founder & CEO of VP0. Published 2026-06-10. 10 min read.
> Source: https://vp0.com/blogs/tinder-card-swipe-animation-react-native-reanimated-free-ios-template-vibe-codin

A swipe card is a pan gesture, a shared value, and an animated style. Reanimated keeps it on the UI thread so it stays smooth even mid-swipe.

**TL;DR.** A Tinder-style swipe card in React Native uses Reanimated for the animation and Gesture Handler for the drag: a pan gesture updates a shared value, an animated style maps it to position and rotation, and on release the card springs back or flies off and fires a like callback through runOnJS. The animation is short; getting the card design right is the slow part, so starting from a free VP0 design and letting Claude Code or Cursor read its source page is the fastest route to a polished result.

A Tinder-style swipe card in React Native is built with two libraries working together: [Reanimated](https://docs.swmansion.com/react-native-reanimated/) drives the animation on the UI thread, and [Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler/) reads the drag. A pan gesture updates a shared value, an animated style maps that value to the card's position and tilt, and on release the card either springs back or flies off the screen. The animation logic is short; getting the card design right is the slower part, which is why starting from a free VP0 design and letting Claude Code or Cursor read its source page is the fastest way to a polished result.

Reanimated, maintained by Software Mansion with more than 10,000 stars, runs the whole gesture on the UI thread through worklets, so the card stays smooth at 60fps, or 120fps on a ProMotion screen, even when the JavaScript thread is busy. That is the reason to use it over a plain `Animated` approach for something this interactive.

## How do you build a Tinder swipe card animation in React Native?

You combine a pan gesture from Gesture Handler with shared values and an animated style from Reanimated. The drag updates a `translateX` and `translateY` shared value, an animated style turns those into a transform with a little rotation, and the gesture's end handler decides whether the card was swiped far enough to count.

The structure is the same every time. Wrap your app in a `GestureHandlerRootView`, define the shared values, attach a `Gesture.Pan()` through a `GestureDetector`, and render the card as an `Animated.View`. Everything that runs during the drag is a worklet, which is what keeps it on the UI thread. The [Reanimated docs](https://docs.swmansion.com/react-native-reanimated/) explain worklets in depth, but you mostly need to know that gesture callbacks and animated styles run there, and that reaching back into normal JavaScript from inside them requires `runOnJS`.

Both libraries work in [Expo](https://docs.expo.dev/) with no native code of your own, so this runs in a managed Expo project as well as a bare React Native one.

## The core: shared values, pan gesture, and animated style

The heart of the card is a shared value for position and an animated style that adds rotation as the card moves. This is the part that makes it feel like Tinder rather than a flat drag.

```jsx
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);

const pan = Gesture.Pan()
  .onUpdate((e) => {
    translateX.value = e.translationX;
    translateY.value = e.translationY;
  })
  .onEnd((e) => {
    if (Math.abs(translateX.value) > SWIPE_THRESHOLD) {
      const dir = Math.sign(translateX.value);
      translateX.value = withSpring(dir * SCREEN_WIDTH * 1.5);
      runOnJS(onSwipe)(dir > 0 ? "like" : "nope");
    } else {
      translateX.value = withSpring(0);
      translateY.value = withSpring(0);
    }
  });

const cardStyle = useAnimatedStyle(() => {
  const rotate = interpolate(
    translateX.value,
    [-SCREEN_WIDTH, 0, SCREEN_WIDTH],
    [-12, 0, 12]
  );
  return {
    transform: [
      { translateX: translateX.value },
      { translateY: translateY.value },
      { rotate: `${rotate}deg` },
    ],
  };
});
```

The `interpolate` call is what sells the effect. Mapping horizontal position to a small rotation, around twelve degrees at the screen edge, gives the card the natural tilt as it leaves the stack. Because all of this runs as worklets on the UI thread, the rotation tracks your finger exactly even mid-animation.

## Snapping the card: thresholds, spring, and the like callback

On release, the card either commits to a swipe or returns to center, decided by how far it traveled. A threshold of roughly a quarter of the screen width feels right for most cards; below it the card springs home, above it the card flies off in that direction.

The key detail is `runOnJS`. The gesture handler runs on the UI thread, but advancing your card stack and firing a "like" or "nope" callback happens in normal React state, on the JavaScript thread. Calling that callback directly from inside the worklet crashes or silently fails, so it has to be wrapped in `runOnJS`, as in the `onEnd` handler above. This single rule is behind a large share of swipe bugs, and a focused read on [fixing Reanimated errors from AI-generated code](/blogs/fixing-claude-react-native-reanimated-errors/) covers the variations of it.

A distance threshold alone feels stiff, because a fast flick that barely moves the card should still count. Gesture Handler gives you `velocityX` in the `onEnd` event, so a better check commits the swipe when either the distance passes the threshold or the velocity is high enough, which matches how a real flick feels. You can also pass that velocity into `withSpring` as its `velocity` option so the card leaves the screen carrying the speed of your finger rather than starting from rest.

For the fly-off, `withSpring` gives a bouncy exit and `withTiming` gives a faster linear one; most dating-style cards use a quick spring. After the animation, reset the shared values to zero for the next card so the new top card starts centered rather than mid-flight.

## Like and nope labels and the card stack

The "LIKE" and "NOPE" labels that fade in as you drag are another `interpolate`, this time on opacity. Map `translateX` from zero to the threshold onto an opacity from zero to one, so the label appears as the card commits to a direction.

```jsx
const likeStyle = useAnimatedStyle(() => ({
  opacity: interpolate(translateX.value, [0, SWIPE_THRESHOLD], [0, 1]),
}));
```

The stack itself is simpler than it looks. Render only the top two or three cards, with the cards behind scaled down slightly and offset so they peek out. When the top card swipes away, remove it from your data array and the next card becomes the top. Rendering the whole deck at once is the common performance mistake; a deck of two or three visible cards keeps the screen light. A broader set of [dating app swipe UI components](/blogs/dating-app-swipe-ui-components/) shows how the stack, labels, and match flow fit together.

## Making it smooth with AI builders and a real design

AI builders scaffold the gesture quickly but get the worklet rules wrong in predictable ways. Claude Code and Cursor will produce a plausible `Gesture.Pan()` and animated style, then call a JavaScript function directly from inside a worklet without `runOnJS`, or capture a stale value because they treated a shared value like normal state. The code looks right and fails at runtime.

The fix is the same pattern that works elsewhere: give the model a real design and tight rules. Starting from a free VP0 design means the card layout, spacing, and label placement are already decided, so the model implements a real card instead of inventing one, and you spend your time on the gesture logic rather than the visuals. Each VP0 design has a machine-readable source page Claude Code, Cursor, or Rork reads from a pasted link, so the generated card matches a real layout. The animation patterns carry over to other gestures too; a [swipe-to-buy button animation](/blogs/swipe-to-buy-button-animation-react-native/), a [smooth Reanimated bottom sheet](/blogs/smooth-reanimated-bottom-sheet-template/), and the [Tinder super like star animation](/blogs/tinder-super-like-star-animation-react-native-free-ios-template-vibe-coding-guid/) that sits on top of this deck all use the same shared-value and worklet foundation.

For app-style interfaces like a dating or discovery feed, this design-first approach pays off most, because the card is the product and its polish is what users judge.

## Common Reanimated swipe bugs

A handful of issues account for most broken swipe cards, and each has a quick fix. The Babel plugin is the first: on older setups, `react-native-reanimated/plugin` must be the last entry in `babel.config.js`, and if it is missing or out of order, animations silently do nothing. Recent Expo SDKs include it through `babel-preset-expo`, so check which setup you are on before adding it twice.

Calling JavaScript from a worklet without `runOnJS` is the second and most common, usually showing up the moment you wire the like callback. Reading or mutating a shared value on the JavaScript thread, rather than inside a worklet or animated style, is the third, and it produces values that never update. Forgetting `GestureHandlerRootView` at the root is the fourth, and it makes gestures do nothing at all on some platforms.

Memory issues are the subtler category, often from not resetting shared values or holding references across card transitions. A dedicated walkthrough of a [Tinder swipe memory leak in React Native](/blogs/tinder-swipe-memory-leak-react-native-fix/) covers how those show up under heavy swiping and how to confirm them.

## Should you use a swipe library instead?

A prebuilt library can be the right call, and it is worth weighing before you build from scratch. Packages like react-native-deck-swiper give you a working swipe deck in minutes, which is perfect when you need a standard Tinder-style stack and nothing unusual. They cover the common case well and save real time.

Building it yourself with Reanimated and Gesture Handler wins when you need custom behavior, a non-standard card shape, extra gestures, a specific physics feel, or tight integration with the rest of your animated UI. A library's defaults are harder to bend than your own code. The honest split is that a library is faster for the standard deck, and a hand-built version is better when the interaction is part of what makes your app distinct. Most teams start with a library and rewrite the deck only when its limits start to bite.

## Key takeaways: a Reanimated swipe card that feels right

Use Gesture Handler for the pan and Reanimated for the animation, keep the gesture work in worklets on the UI thread, and wrap any JavaScript callback in `runOnJS`. Add rotation and label opacity with `interpolate`, decide swipes with a screen-width threshold, and render only the top few cards in the stack. Let an AI builder scaffold the gesture from a real design, then check it for the worklet mistakes it tends to make. Starting from a free VP0 design gives you a polished card for nothing, while a commissioned swipe feature can run $5,000 or more once design and animation are both accounted for.

You can [browse VP0 designs](/explore) to start your swipe card from a real screen rather than a blank component.

## Frequently asked questions

### How do you make a Tinder swipe animation in React Native with Reanimated?

Combine a `Gesture.Pan()` from Gesture Handler with Reanimated shared values and an animated style. The drag updates `translateX` and `translateY`, the animated style maps them to a transform plus a small rotation through `interpolate`, and the gesture's end handler springs the card back if it did not pass a threshold or flies it off-screen if it did, calling your like or nope callback through `runOnJS`. Starting the card from a free VP0 design means the layout is settled and you only build the gesture.

### What is the safest way to build a swipe card with Claude Code or Cursor?

Constrain the model and review the worklet code. Tell it to keep gesture and animation logic in worklets, to wrap every JavaScript callback in `runOnJS`, and never to read shared values on the JavaScript thread. AI reliably gets these rules wrong, producing code that compiles but fails at runtime, so a manual pass over the gesture handler is essential. Giving it a real VP0 design to implement keeps the card visuals correct so you can focus on the logic.

### Can VP0 provide a free React Native template for a swipe card UI?

Yes. VP0 is a free iOS app design library where every design has a machine-readable source page an AI builder reads from a pasted link, with React Native and SwiftUI variants. You start from the card design, hand its source to Claude Code, Cursor, or Rork, and build the Reanimated gesture on top, rather than designing the card and coding the animation from a blank prompt.

### Why use Reanimated instead of the Animated API for swipe cards?

Reanimated runs gestures and animations on the UI thread through worklets, so the card tracks your finger and animates at 60fps, or 120fps on ProMotion displays, even when the JavaScript thread is busy rendering the next card. The built-in `Animated` API runs more of its work on the JavaScript thread, which causes dropped frames during exactly the kind of continuous gesture a swipe card depends on.

### What common errors happen when vibe coding a Reanimated swipe card?

The frequent ones are calling a JavaScript function from a worklet without `runOnJS`, a missing or misordered Reanimated Babel plugin so animations do nothing, reading shared values on the wrong thread, and forgetting `GestureHandlerRootView` at the app root. There are also memory issues from not resetting shared values between cards. All of them are quick to fix once you recognize them, but they are easy to ship because the code looks correct.

## Frequently asked questions

### How do you make a Tinder swipe animation in React Native with Reanimated?

Combine a Gesture.Pan() from Gesture Handler with Reanimated shared values and an animated style. The drag updates translateX and translateY, the animated style maps them to a transform plus a small rotation through interpolate, and the gesture's end handler springs the card back if it did not pass a threshold or flies it off-screen if it did, calling your like or nope callback through runOnJS. Starting the card from a free VP0 design means the layout is settled and you only build the gesture.

### What is the safest way to build a swipe card with Claude Code or Cursor?

Constrain the model and review the worklet code. Tell it to keep gesture and animation logic in worklets, to wrap every JavaScript callback in runOnJS, and never to read shared values on the JavaScript thread. AI reliably gets these rules wrong, producing code that compiles but fails at runtime, so a manual pass over the gesture handler is essential. Giving it a real VP0 design to implement keeps the card visuals correct so you can focus on the logic.

### Can VP0 provide a free React Native template for a swipe card UI?

Yes. VP0 is a free iOS app design library where every design has a machine-readable source page an AI builder reads from a pasted link, with React Native and SwiftUI variants. You start from the card design, hand its source to Claude Code, Cursor, or Rork, and build the Reanimated gesture on top, rather than designing the card and coding the animation from a blank prompt.

### Why use Reanimated instead of the Animated API for swipe cards?

Reanimated runs gestures and animations on the UI thread through worklets, so the card tracks your finger and animates at 60fps, or 120fps on ProMotion displays, even when the JavaScript thread is busy rendering the next card. The built-in Animated API runs more of its work on the JavaScript thread, which causes dropped frames during exactly the kind of continuous gesture a swipe card depends on.

### What common errors happen when vibe coding a Reanimated swipe card?

The frequent ones are calling a JavaScript function from a worklet without runOnJS, a missing or misordered Reanimated Babel plugin so animations do nothing, reading shared values on the wrong thread, and forgetting GestureHandlerRootView at the app root. There are also memory issues from not resetting shared values between cards. All of them are quick to fix once you recognize them, but they are easy to ship because the code looks correct.

---
*Published on the [VP0 Journal](https://vp0.com/blogs). Free to read, index and cite with attribution.*
