Carousel
Virtualized carousel driven by data.
Installation
npx axiom add carouselpnpm dlx axiom add carouselyarn dlx axiom add carouselbun x axiom add carouselInstalls: react-native-reanimated.
Scrolling, snapping, autoplay and the imperative API live in use-carousel.ts, on top of a virtualized FlatList.
Carousel holds the data and the scroll position. What you see is made of parts you place: Carousel.List for the items, and, only if you want them, Carousel.Pagination and Carousel.AutoPlay. A carousel without dots or autoplay runs no code for them.
Usage
The root follows the list model: pass data and a render function. Carousel.List draws them.
import { Carousel } from "@/components/ui/carousel";
<Carousel
data={stories}
renderItem={({ item }) => <StoryCard story={item} />}
keyExtractor={(item) => item.id}
>
<Carousel.List />
<Carousel.Pagination />
</Carousel>;Props
Carousel
| Prop | Type | Default | Description |
|---|---|---|---|
data | T[] | — | Items to render. |
renderItem | ({ item, index, progress }) => ReactElement | — | Renders one item. progress is a shared value from −1 to 1 as the item moves through the center, for scale or parallax effects. |
keyExtractor | (item: T, index: number) => string | index | Stable key per item. |
itemWidth | number | screen width − margins | Width of each item. Smaller than the viewport, the next item peeks in. |
gap | SpacingToken | 3 | Space between items. |
contentInset | SpacingToken | screenMargin | Padding at the start and end, so the first item aligns with the rest of the screen. |
snap | 'item' | 'page' | 'none' | 'item' | item stops on each item, page on every visible page, none scrolls freely. |
index | number | — | Controlled active index. |
onIndexChange | (index: number) => void | — | Called when the carousel settles on a new item. |
haptic | HapticKind | false | — | Haptic played when a swipe lands on another item. Off unless you pass a kind. |
ref | Ref<CarouselRef> | — | scrollToIndex(index, animated?), next(), prev(). next() stops at the last item. |
children | ReactNode | — | Carousel.List and the other parts, in the order you want them. |
style | StyleProp<ViewStyle> | — | Extra styles for the container. |
Parts
| Part | What it is |
|---|---|
Carousel.List | The horizontal FlatList. Takes windowSize (items kept mounted around the visible ones, 5), initialNumToRender, accessibilityLabel, style and contentContainerStyle. |
Carousel.Pagination | One dot per item, the active one stretched into a pill as the list scrolls. A View: place it above or below the list. |
Carousel.AutoPlay | Renders nothing. Moves to the next item every interval ms, skips a tick while the list is touched, and stays off with Reduce Motion. loop goes back to the first item. |
useCarousel() returns the state of the carousel around it: active, count, scrollX, interval and scrollToIndex. Use it for your own indicator, such as a 2 / 8 counter:
function Counter() {
const { active, count } = useCarousel();
return (
<Text variant="footnote" color="muted">
{active + 1} / {count}
</Text>
);
}useCarouselAutoPlay({ interval, loop }) is the hook behind Carousel.AutoPlay, for a component of yours inside the carousel.
Haptics
Pass a kind, usually "selection", to play a haptic when a swipe lands on another item. Carousel.AutoPlay and ref.current.next() play nothing. Keep it for carousels the user pages through one item at a time, like an onboarding: on a fast gallery it gets tiring. Nothing plays until your app connects a haptics library: see Haptics.
Use cases
Onboarding
Full-width pages with snap="page", dots, and a button that follows the index.
const ref = useRef<CarouselRef>(null);
const [index, setIndex] = useState(0);
<Carousel ref={ref} data={slides} snap="page" onIndexChange={setIndex} renderItem={renderSlide}>
<Carousel.List />
<Carousel.Pagination />
</Carousel>
<Button size="lg" fullWidth onPress={() => (index < slides.length - 1 ? ref.current?.next() : finish())}>
{index < slides.length - 1 ? 'Next' : 'Get started'}
</Button>Product gallery
Photos with a counter instead of dots when there are many. The counter is the Counter component above, laid over the photo.
<Carousel
data={product.photos}
snap="page"
renderItem={({ item }) => <Image source={item} style={styles.photo} />}
>
<Carousel.List accessibilityLabel="Product photos" />
<View style={styles.counter}>
<Counter />
</View>
</Carousel>Stories row
Small round items, free scroll with snap="none", and only nearby items mounted.
<Carousel data={stories} itemWidth={64} gap={3} snap="none" renderItem={renderStory}>
<Carousel.List windowSize={10} />
</Carousel>Guidelines
Keep one main scroll direction per area. A horizontal carousel inside a vertical feed is fine; stacking several scroll directions in the same space is not. See Mobile design guidelines.