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.

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 🧠
FlatListis used to render the images as a horizontal list.- The
renderItemprop defines how each item is displayed—in this case, anImage. keyExtractorensures each item has a unique key; here we use the index.- Adding the
horizontalprop enables horizontal scrolling. - Setting
showHorizontalScrollIndicatorhides the scroll bar.
Adding the Carousel to the App 🧩
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>
);
}
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
CarouselTilecomponent takes anitemprop, which contains the image data. - We use the
useWindowDimensionshook to set the tile’s width equal to the screen width. - Center the image using
justifyContent: 'center'andalignItems: 'center'.
Adding the CarouselTile to the Carousel 🔄
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>
);
}
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:
| Prop | Purpose |
|---|---|
pagingEnabled | Snaps to full "pages" (i.e., item width) |
snapToInterval | Custom snapping behavior (same as screen width here) |
snapToAlignment | Aligns snapped item to the center of the list |
decelerationRate | Makes swiping faster and feel more natural |
scrollEventThrottle | Controls how often scroll events fire (16ms = ~60fps) |
windowSize | Number of items to render outside the visible area |
onScroll | Hooking 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 🧠
useAnimatedStylecreates dynamic styles that respond to the scroll position.interpolatecalculates scale and translation values for each tile based on its index.Extrapolation.CLAMPensures animation values don’t go out of bounds.translateXcreates a parallax effect.scalegives 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>
);Pass Props from Carousel to CarouselTile 🧩
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.

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 🧠
CarouselIndicatorcomponent takesitemsCountandcurrentIndexas 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! 🎉

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