Custom Animations

Custom Animations

v5 has two complementary animation surfaces:

  • itemAnimation controls the outer style of every slide;
  • render-item relativeProgress lets content inside a slide animate independently.

Both are UI-thread/worklet surfaces. layout and itemAnimation are mutually exclusive.

itemAnimation

import { interpolate } from "react-native-reanimated";
import {
  Carousel,
  type CarouselItemAnimation,
} from "react-native-reanimated-carousel";
 
const itemAnimation: CarouselItemAnimation = (relativeProgress) => {
  "worklet";
 
  return {
    opacity: interpolate(relativeProgress, [-1, 0, 1], [0.4, 1, 0.4]),
    transform: [
      {
        scale: interpolate(relativeProgress, [-1, 0, 1], [0.85, 1, 0.85]),
      },
    ],
  };
};
 
<Carousel
  data={data}
  renderItem={({ item }) => <Slide item={item} />}
  itemAnimation={itemAnimation}
/>;

The callback signature is:

type CarouselItemAnimation = (
  relativeProgress: number,
  index: number
) => ViewStyle;

It must be worklet-compatible. Avoid JS-only APIs, React state, and side effects inside it.

Relative progress semantics

For each item:

  • 0 means selected;
  • a positive value means logically forward;
  • a negative value means logically backward;
  • fractions represent movement between items;
  • values are not clamped;
  • loop mode uses the nearest equivalent copy and remains continuous.

The sign is the same for horizontal, vertical, LTR, and RTL carousels. This makes opacity, scale, and logical sequencing portable across directions.

Animating content inside a slide

renderItem receives a SharedValue<number>:

import Animated, {
  interpolate,
  useAnimatedStyle,
} from "react-native-reanimated";
import type { SharedValue } from "react-native-reanimated";
 
function AnimatedCaption({
  relativeProgress,
  title,
}: {
  relativeProgress: SharedValue<number>;
  title: string;
}) {
  const style = useAnimatedStyle(() => ({
    opacity: interpolate(
      Math.abs(relativeProgress.value),
      [0, 1],
      [1, 0]
    ),
    transform: [
      {
        translateY: interpolate(
          relativeProgress.value,
          [-1, 0, 1],
          [24, 0, -24]
        ),
      },
    ],
  }));
 
  return <Animated.Text style={style}>{title}</Animated.Text>;
}
 
<Carousel
  data={data}
  renderItem={({ item, relativeProgress }) => (
    <AnimatedCaption title={item.title} relativeProgress={relativeProgress} />
  )}
/>;

Do not read relativeProgress.value during React render. Read it from a Reanimated worklet such as useAnimatedStyle or useDerivedValue.

Horizontal RTL

Logical progress is normalized, but React Native transforms are physical. If a custom effect moves items on the x-axis, map its sign for RTL:

import { I18nManager } from "react-native";
import { interpolate } from "react-native-reanimated";
import type { CarouselItemAnimation } from "react-native-reanimated-carousel";
 
const rtlSign = I18nManager.isRTL ? -1 : 1;
 
const itemAnimation: CarouselItemAnimation = (relativeProgress) => {
  "worklet";
 
  const logicalX = interpolate(
    relativeProgress,
    [-1, 0, 1],
    [-80, 0, 80]
  );
 
  return {
    transform: [{ translateX: logicalX * rtlSign }],
  };
};

Built-in normal, parallax, and horizontal-stack layouts perform this mapping internally. Do not reverse data or mirror the whole carousel with scaleX.

Vertical effects do not depend on RTL.

Animation sanitization

Carousel sanitizes the returned style before applying it:

  • invalid or non-finite style values are removed;
  • zIndex is normalized to a finite integer;
  • item dimensions remain owned by Carousel.

Use finite interpolation output and keep expensive calculations outside the callback when they can be precomputed.

Transition animation

itemAnimation describes slide appearance. The separate animation prop describes how the carousel moves to a settled position:

<Carousel
  data={data}
  renderItem={renderItem}
  itemAnimation={itemAnimation}
  animation={{ type: "timing", duration: 350 }}
/>

The transition configuration applies to gesture snapping, imperative commands, and autoplay. For a spring:

<Carousel
  data={data}
  renderItem={renderItem}
  itemAnimation={itemAnimation}
  animation={{
    type: "spring",
    damping: 18,
    stiffness: 180,
  }}
/>

Reanimated applies the operating system's reduce-motion preference by default.

Built-in layout or custom animation

Use a built-in layout when it covers the effect:

<Carousel
  data={data}
  renderItem={renderItem}
  layout={{ type: "parallax", offset: 80, scale: 0.9 }}
/>

Use itemAnimation when the slide style needs a custom mapping. Passing both is a TypeScript error and a runtime error for untyped JavaScript.