React Native animated carousel tutorial: build an image slider with FlatList and reanimated


As the result of this tutorial we will create a carousel that will look and operate like this.

final result

I assume you already have an Expo app set up. If not, please refer to my previous post on how to set up a React Native app with Expo and TypeScript.

The complete code for this tutorial is available on GitHub

Setting up the project 🚀

First create a new component called Carousel.tsx in your components folder. This component will be responsible for rendering the carousel.

import { FlatList, ImageSourcePropType, View, Image } from "react-native";
 
export default function Carousel() {
  // I use local images for the carousel. You can use remote images as well.
  const image1: ImageSourcePropType = require("@/assets/images/GGIY.jpg");
  ...
 
  const images = [image1, image2, image3, image4];
 
  return (
    <View
      style={{
        width: "100%",
      }}
    >
      <FlatList
        data={images}
        renderItem={({ item }) => (
          <View style={{ height: "100%", justifyContent: "center" }}>
            <Image source={item} style={{ width: 250, height: 250 }} />
          </View>
        )}
        keyExtractor={(_, index) => index.toString()}
        horizontal
        showsHorizontalScrollIndicator={false}
      />
    </View>
  );
}

Explanation 🧠

  • FlatList is used to render the images as a horizontal list.
  • The renderItem prop defines how each item is displayed—in this case, an Image.
  • keyExtractor ensures each item has a unique key; here we use the index.
  • Adding the horizontal prop enables horizontal scrolling.
  • Setting showHorizontalScrollIndicator hides the scroll bar.

Now, let’s use the Carousel component in our main screen. Import Carousel into your index.tsx (or wherever your main view is) and render it there.

import Carousel from "@/components/Carousel";
import { View } from "react-native";
 
export default function HomeScreen() {
  return (
    <View style={{ height: "100%" }}>
      <Carousel />
    </View>
  );
}
flatlist with images

Create CarouselTile component 🧱

Before adding animations, we’ll refine the tiles inside the carousel. Let’s create a new component called CarouselTile.tsx in the components folder. This component will render each individual tile in the carousel.

import {
  Image,
  ImageSourcePropType,
  useWindowDimensions,
  View,
} from "react-native";
 
export const CarouselTile = ({ item }: { item: ImageSourcePropType }) => {
  return (
    <View
      style={{
        justifyContent: "center",
        alignItems: "center",
        width: width,
      }}
    >
      <Image source={item} style={{ width: 250, height: 250 }} />
    </View>
  );
};

Explanation 🧠

  • The CarouselTile component takes an item prop, which contains the image data.
  • We use the useWindowDimensions hook to set the tile’s width equal to the screen width.
  • Center the image using justifyContent: 'center' and alignItems: 'center'.

Next, update the Carousel component to render CarouselTile instead of using a plain Image component.

import { CarouselTile } from "./CarouselTile"; // import the CarouselTile component
 
export default function Carousel() {
  ...
  return (
    <View
      style={{
        width: "100%",
        height: "100%",
        overflow: "visible",
        justifyContent: "center",
      }}
    >
      <FlatList
        data={images}
        renderItem={({ item }) => <CarouselTile item={item} />} // replace Image with CarouselTile
        keyExtractor={(_, index) => index.toString()}
        horizontal
        showsHorizontalScrollIndicator={false}
      />
    </View>
  );
}
one image at a time

Adding Animations ✨

Now the carousel shows one image at a time. Let’s animate the transitions!

Start by capturing scroll position:

import {
  useAnimatedScrollHandler,
  useSharedValue,
} from "react-native-reanimated";
 
...
 
export default function Carousel() {
  ...
 
const scrollX = useSharedValue(0);
 
const onScrollHandler = useAnimatedScrollHandler({
  onScroll: (event) => {
    scrollX.value = event.contentOffset.x;
  },
});
 
const { width } = useWindowDimensions();
 
...
}

Switch from FlatList to Animated.FlatList by importing it from react-native-reanimated.

<Animated.FlatList
  data={images}
  renderItem={({ item }) => <CarouselTile item={item} />}
  ...
  pagingEnabled
  snapToInterval={width}
  snapToAlignment="center"
  decelerationRate="fast"
  scrollEventThrottle={16}
  windowSize={3}
  onScroll={onScrollHandler}
/>

🔑 Breakdown of Key Props:

PropPurpose
pagingEnabledSnaps to full "pages" (i.e., item width)
snapToIntervalCustom snapping behavior (same as screen width here)
snapToAlignmentAligns snapped item to the center of the list
decelerationRateMakes swiping faster and feel more natural
scrollEventThrottleControls how often scroll events fire (16ms = ~60fps)
windowSizeNumber of items to render outside the visible area
onScrollHooking into the animated scroll position for custom animations

Animate the CarouselTile 🎞️

You will not yet see the animations, because we need to add them to the CarouselTile component. Let's do that now.

Update the CarouselTile component to accept index and scrollX props:

export const CarouselTile = ({
  item,
  index,
  scrollX,
}: {
  item: ImageSourcePropType;
  index: number;
  scrollX: SharedValue<number>;
}) => {
  ...
}

Then add the animated styles using useAnimatedStyle:

const { width } = useWindowDimensions();
const rnAnimatedStyle = useAnimatedStyle(() => ({
  transform: [
    {
      translateX: interpolate(
        scrollX.value,
        [(index - 1) * width, index * width, (index + 1) * width],
        [-width * 0.4, 0, width * 0.4],
        Extrapolation.CLAMP
      ),
    },
    {
      scale: interpolate(
        scrollX.value,
        [(index - 1) * width, index * width, (index + 1) * width],
        [0.8, 1, 0.8],
        Extrapolation.CLAMP
      ),
    },
  ],
}));

Explanation 🧠

  • useAnimatedStyle creates dynamic styles that respond to the scroll position.
  • interpolate calculates scale and translation values for each tile based on its index.
  • Extrapolation.CLAMP ensures animation values don’t go out of bounds.
  • translateX creates a parallax effect.
  • scale gives a zoom-in effect for the centered tile.

Make sure to wrap the tile in Animated.View instead of View, and apply the animated style.

return (
  <Animated.View
    style={[
      {
        justifyContent: "center",
        alignItems: "center",
        width: width,
      },
      rnAnimatedStyle,
    ]}
  >
    <Image source={item} style={{ width: 250, height: 250 }} />
  </Animated.View>
);

Update the render function in Carousel:

<FlatList
  ...
  renderItem={({ item, index }) => (
    <CarouselTile item={item} index={index} scrollX={scrollX} />
  )}
  ...
/>

You should now see animated transitions in your carousel.

animated carousel

Adding Pagination Indicator 🔘

Let's add a simple position indicator below the carousel. Create a new component called CarouselIndicator.tsx in the components folder.

import { View } from "react-native";
 
export const CarouselIndicator = ({
  itemsCount,
  currentIndex,
}: {
  itemsCount: number,
  currentIndex: number,
}) => {
  return (
    <View
      style={{
        flexDirection: "row",
        height: 60,
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      {Array.from({ length: itemsCount }).map((_, index) => (
        <View
          key={index}
          style={{
            width: 8,
            height: 8,
            borderRadius: 8,
            backgroundColor: index === currentIndex ? "white" : "grey",
            marginHorizontal: 4,
          }}
        />
      ))}
    </View>
  );
};

Explanation 🧠

  • CarouselIndicator component takes itemsCount and currentIndex as props.
  • It renders a row of dots—highlighting the active one in white, and the rest in grey.

Now just import and use CarouselIndicator inside your Carousel component, passing the correct props.

 
// put this function outside of the component
const createViewableItemsChangedHandler = (
  containerLength: number,
  setPaginationIndex: (index: number) => void
) => {
  return ({ viewableItems }: { viewableItems: ViewToken[] }) => {
    const index = viewableItems[0]?.index;
    if (index !== undefined && index !== null) {
      const newIndex = index % containerLength;
      setPaginationIndex(newIndex);
    }
  };
};
 
export default function Carousel() {
  ...
 
  const [paginationIndex, setPaginationIndex] = useState(0);
 
  const onViewableItemsChanged = createViewableItemsChangedHandler(
    images.length,
    setPaginationIndex
  );
 
  const viewabilityConfigCallbackPairs = useRef([
    {
      viewabilityConfig: {
        itemVisiblePercentThreshold: 50,
      },
      onViewableItemsChanged: onViewableItemsChanged,
    },
  ]);
 
  return (
    <View
      ...
    >
      <Animated.FlatList
       ...
        viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
      />
      <CarouselIndicator
        itemsCount={images.length}
        currentIndex={paginationIndex}
      />
    </View>
  );
}

And that's it! 🎉

final result

You now have a fully functional, animated image carousel in React Native with:

  • Horizontal scroll
  • Snapping
  • Parallax and scale animations
  • Pagination dots

Feel free to customize and build on it for your own projects!


📚 References & further reading

This guide is partially based on the tutorial from Pradip Debnath