How to build an animated carousel in React Native
By the LumioUI team · Updated
A carousel is a row of pages you swipe through one at a time, with dots that show where you are. In React Native, put the pages in a horizontal ScrollView with pagingEnabled, read the scroll offset into a Reanimated shared value, and derive each page's scale and each dot's width from it on the UI thread. The LumioUI Carousel stretches its dots as you swipe and fades side pages back so each new one arrives.
Recording of the LumioUI Carousel component (Pro).
How an animated carousel works
- Paging. A horizontal
ScrollView(orFlatListfor long lists) withpagingEnabledsnaps to whole pages. Each page is the width of the carousel. - One shared value.
useAnimatedScrollHandlerwrites the offset to a shared value without going through React state, so nothing re-renders while you swipe and the motion stays at 60fps. - Interpolate per page.
offset / widthis the page position as a number (1.5 = halfway between pages 1 and 2). Each page and dot maps its distance from that number to scale, opacity or width. - Report the page on settle. Update React state (the current index) only when a swipe ends, not on every frame.
- Accessibility. Give the carousel an
adjustablerole, or label each page "Photo 2 of 5", so screen reader users know there is more to swipe.
import * as React from "react";
import { Image, StyleSheet, View, useWindowDimensions } from "react-native";
import Animated, {
Extrapolation, interpolate, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue,
type SharedValue,
} from "react-native-reanimated";
export function PhotoCarousel({ photos }: { photos: string[] }) {
const { width } = useWindowDimensions();
const x = useSharedValue(0); // scroll offset, read on the UI thread
const onScroll = useAnimatedScrollHandler((e) => { x.value = e.contentOffset.x; });
return (
<View>
<Animated.ScrollView horizontal pagingEnabled showsHorizontalScrollIndicator={false}
onScroll={onScroll} scrollEventThrottle={16}>
{photos.map((uri, i) => <Page key={uri} uri={uri} i={i} x={x} width={width} />)}
</Animated.ScrollView>
<View style={styles.dots}>
{photos.map((uri, i) => <Dot key={uri} i={i} x={x} width={width} />)}
</View>
</View>
);
}
function Page({ uri, i, x, width }: { uri: string; i: number; x: SharedValue<number>; width: number }) {
// Pages beside the current one recede: a little smaller and dimmer.
const style = useAnimatedStyle(() => {
const p = interpolate(x.value / width, [i - 1, i, i + 1], [0, 1, 0], Extrapolation.CLAMP);
return { opacity: 0.6 + 0.4 * p, transform: [{ scale: 0.92 + 0.08 * p }] };
});
return (
<Animated.View style={[{ width, padding: 16 }, style]}>
<Image source={{ uri }} style={styles.photo} accessibilityIgnoresInvertColors />
</Animated.View>
);
}
function Dot({ i, x, width }: { i: number; x: SharedValue<number>; width: number }) {
// The active dot stretches into a pill, following the finger.
const style = useAnimatedStyle(() => {
const p = interpolate(x.value / width, [i - 1, i, i + 1], [0, 1, 0], Extrapolation.CLAMP);
return { width: 6 + 12 * p, opacity: 0.35 + 0.65 * p };
});
return <Animated.View style={[styles.dot, style]} />;
}
const styles = StyleSheet.create({
photo: { width: "100%", aspectRatio: 4 / 5, borderRadius: 20 },
dots: { flexDirection: "row", justifyContent: "center", gap: 6, marginTop: 4 },
dot: { height: 6, borderRadius: 3, backgroundColor: "#111" },
}); For long lists, swap the ScrollView for Animated.FlatList so only nearby pages stay mounted.
The quick way: LumioUI Carousel
The Carousel adds stretching page dots, side pages that recede, pinch and double-tap zoom, a glass pager for photos, and only keeps the pages near the visible one mounted. It's part of LumioUI Pro.
import { Image } from "react-native";
import { Carousel } from "@/components/ui/carousel/carousel";
export function ListingPhotos({ photos }: { photos: string[] }) {
return (
<Carousel
data={photos}
renderItem={(uri) => (
<Image source={{ uri }} className="aspect-[4/5] w-full rounded-3xl" />
)}
onIndexChange={(i) => console.log("page", i)}
pinchToZoom
/>
);
} - Pass
dataandrenderItem, or plain children as pages. indexandonIndexChangemake it controlled.showDots(default on) andglassstyle the pager.pinchToZoomandmaxZoomturn it into a photo viewer.renderWindowsets how many pages stay mounted on each side.- For product pages with thumbnails see Product Gallery; for first-run screens, Onboarding Pager.
Add it to your app
LumioUI's CLI copies the component's source into your project, so it's yours to read and change. Run init once per project:
npx @lumioui/cli init
npx @lumioui/cli add pro/carousel It works in Expo (SDK 54+) and bare React Native, with Reanimated 4 and NativeWind. See the installation guide and the full Carousel API.Carousel is part of LumioUI Pro: Pro installs need your licence key (CLI guide, pricing).
Related components
- Onboarding Pager (Pro)
- Product Gallery (Pro)
- Pagination
- Image
More React Native guides
- How to make a bottom sheet in React Native
- Animated tabs with a sliding indicator in React Native
- How to show toast notifications in React Native
- Skeleton loading screens in React Native
- How to add dark mode and a theme toggle in React Native
- How to build an accordion in React Native
- How to make a side drawer menu in React Native
- How to make an animated heart like button in React Native
- How to make a modal dialog in React Native
- How to make an animated splash screen in React Native and Expo
- How to build a month calendar in React Native