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 🧠
FlatList
is used to render the images as a horizontal list.- The
renderItem
prop defines how each item is displayed—in this case, anImage
. 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.
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
CarouselTile
component takes anitem
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'
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 🧠
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>
);
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 🧠
CarouselIndicator
component takesitemsCount
andcurrentIndex
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! 🎉

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