Skip to content

React Native Animation Recipes

A React Native guide for coding agents. Also covers reanimated code examples, gesture handler examples, animated press, swipe action, scroll animation, reduced motion code, and 1 more.

Show all 7 aliases

reanimated code examples, gesture handler examples, animated press, swipe action, scroll animation, reduced motion code, animation patterns

Use motion to confirm input, explain spatial change, or preserve continuity. These recipes target React Native Gesture Handler 3's hook API and the public Reanimated APIs.

Build Accessible UI-Thread Press Feedback

Keep the visual press state on the UI thread, then cross to JavaScript only for the application callback. Expose an accessibility action because a gesture is never the only way to activate a control.

import { GestureDetector, useTapGesture } from "react-native-gesture-handler";
import Animated, {
  runOnJS,
  useAnimatedStyle,
  useReducedMotion,
  useSharedValue,
  withTiming,
} from "react-native-reanimated";
import { Text, type StyleProp, type TextStyle, type ViewStyle } from "react-native";

type MotionButtonProps = {
  label: string;
  onPress(): void;
  style?: StyleProp<ViewStyle>;
  textStyle?: StyleProp<TextStyle>;
};

export function MotionButton({ label, onPress, style, textStyle }: MotionButtonProps) {
  const pressed = useSharedValue(false);
  const reduceMotion = useReducedMotion();

  const tap = useTapGesture({
    onBegin: () => pressed.set(true),
    onFinalize: (event) => {
      pressed.set(false);
      if (!event.canceled) runOnJS(onPress)();
    },
  });

  const animatedStyle = useAnimatedStyle(() => ({
    opacity: withTiming(pressed.get() ? 0.72 : 1, { duration: 100 }),
    transform: [
      { scale: withTiming(pressed.get() && !reduceMotion ? 0.97 : 1, { duration: 100 }) },
    ],
  }));

  return (
    <GestureDetector gesture={tap}>
      <Animated.View
        accessible
        accessibilityRole="button"
        accessibilityLabel={label}
        accessibilityActions={[{ name: "activate", label }]}
        onAccessibilityAction={(event) => {
          if (event.nativeEvent.actionName === "activate") onPress();
        }}
        style={[
          { minHeight: 44, paddingHorizontal: 16, justifyContent: "center" },
          style,
          animatedStyle,
        ]}
      >
        <Text style={[{ textAlign: "center" }, textStyle]}>{label}</Text>
      </Animated.View>
    </GestureDetector>
  );
}

For a basic button, prefer Pressable; use this pattern only when UI-thread feedback materially improves the interaction.

Make a Horizontal Swipe Coexist with Vertical Scrolling

Use activation and failure offsets to resolve intent. Combine distance and velocity, and expose the committed action through a visible button or menu too.

import type { ReactNode } from "react";
import { GestureDetector, usePanGesture } from "react-native-gesture-handler";
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated";
import { View } from "react-native";

const LIMIT = 96;

export function SwipeReveal({ children, action }: { children: ReactNode; action: ReactNode }) {
  const offsetX = useSharedValue(0);
  const startX = useSharedValue(0);

  const pan = usePanGesture({
    activeOffsetX: [-10, 10],
    failOffsetY: [-14, 14],
    onBegin: () => startX.set(offsetX.get()),
    onUpdate: (event) => {
      const proposed = startX.get() + event.translationX;
      offsetX.set(Math.max(-LIMIT, Math.min(0, proposed)));
    },
    onFinalize: (event) => {
      if (event.canceled) {
        offsetX.set(withSpring(0, { damping: 22, stiffness: 260 }));
        return;
      }
      const shouldOpen = offsetX.get() < -LIMIT * 0.45 || event.velocityX < -650;
      offsetX.set(withSpring(shouldOpen ? -LIMIT : 0, { damping: 22, stiffness: 260 }));
    },
  });

  const style = useAnimatedStyle(() => ({
    transform: [{ translateX: offsetX.get() }],
  }));

  return (
    <View>
      <View style={{ position: "absolute", right: 0, width: LIMIT, minHeight: 44 }}>
        {action}
      </View>
      <GestureDetector gesture={pan}>
        <Animated.View style={style}>{children}</Animated.View>
      </GestureDetector>
    </View>
  );
}

Pass a labeled Pressable as action, sized to at least 44 points. It remains available to assistive technology without requiring the swipe. Do not put a destructive action under the swipe unless it is reversible or asks for a precise second commitment.

Derive Scroll Effects from One Progress Value

Store the source scroll position once and derive every visual value from it.

import Animated, {
  Extrapolation,
  interpolate,
  useAnimatedScrollHandler,
  useAnimatedStyle,
  useSharedValue,
} from "react-native-reanimated";

export function CollapsingHeaderList() {
  const scrollY = useSharedValue(0);
  const onScroll = useAnimatedScrollHandler((event) => {
    scrollY.set(event.contentOffset.y);
  });

  const headerStyle = useAnimatedStyle(() => {
    const progress = interpolate(scrollY.get(), [0, 88], [0, 1], Extrapolation.CLAMP);
    return {
      opacity: 1 - progress * 0.3,
      transform: [
        { translateY: -progress * 24 },
        { scale: 1 - progress * 0.06 },
      ],
    };
  });

  return (
    <>
      <Animated.View style={headerStyle}>{/* Header content */}</Animated.View>
      <Animated.FlatList
        data={[]}
        renderItem={() => null}
        onScroll={onScroll}
        scrollEventThrottle={16}
        contentInsetAdjustmentBehavior="automatic"
      />
    </>
  );
}

In production, keep a real virtualized dataset and memoized rows. Avoid React state updates for every scroll event.

Respect Reduced Motion Without Removing Feedback

Reduced motion usually means removing travel, zoom, parallax, and continuous looping. Keep concise opacity, color, or instant state changes when they communicate success or selection. Read the preference once through Reanimated’s useReducedMotion and branch motion values, not the entire feature.

Choose Skia Only for Drawing Problems

Use Skia when the effect requires custom paths, shaders, masks, particles, or canvas rendering. A button scale, card swipe, list header, or layout transition should remain a normal view animation. Keep semantic controls outside or over the canvas so accessibility does not depend on painted pixels.

Animation Review Checks

  • Every motion answers what changed or whether input worked.
  • Only transforms and opacity update per frame unless profiling proves layout animation is safe.
  • Gestures can cancel, reverse, and yield to scrolling.
  • Reduced motion removes displacement and looping.
  • A visible or accessibility action duplicates every gesture-only command.
  • Performance is measured on representative low-end Android hardware.

Sources

API details are checked against Reanimated’s shared values, reduced motion, and spring documentation, plus Gesture Handler 3's pan gesture and migration guides.

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-react-native-guide({ topic: "react-native-animation-recipes" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems