@leonsilicon/react-native-reanimated-carousel
v0.0.0
Published
Simple carousel component.fully implemented using Reanimated 2.Infinitely scrolling, very smooth.
Readme
react-native-reanimated-carousel
The best carousel component in React Native community. ⚡️
v5 beta notes
- Sizing:
stylecontrols the container size;itemWidth/itemHeightcontrol the page size (snap distance & animation progress). - Scroll offset shared value: use
scrollOffsetValue(recommended).defaultScrollOffsetValueis deprecated but still supported. - Progress:
onProgressChangesupports both a callback andSharedValue<number>. - Pagination accessibility:
Pagination.BasicandPagination.CustomsupportpaginationItemAccessibilityfor per-item a11y overrides. - Custom animation safety:
customAnimationstyles are sanitized andzIndexis normalized to finite integers.
📊 Version Compatibility
| Carousel Version | Expo SDK | React Native | Reanimated | Gesture Handler | Worklets | |------------------|----------|--------------|------------|-----------------|------------| | v5.0.0-beta | 54+ | 0.80+ | 4.0.0+ | 2.9.0+ | 0.5.0+ | | v4.x | 50-53 | 0.70.3+ | 3.0.0+ | 2.9.0+ | ❌ | | v3.x | 47-49 | 0.66.0+ | 2.0.0+ | 2.0.0+ | ❌ |
Sponsors
License
MIT
Documentation
A complete, in-tree reference for the <Carousel> component, the <Pagination> helpers, and every prop, mode, callback, imperative method, and behavioral nuance.
Table of Contents
- Installation
- Quick Start
- Sizing model: container vs. page
- Variable-size mode (
getItemWidth/getItemHeight) - Animation modes
- Loop, paging, and snapping
- Auto-play
- Gestures and overscroll
- Programmatic control (
ref) - Tracking progress
<Pagination>indicators- Render-item contract
- Complete prop reference
- TypeScript exports
- Migration notes (v4 → v5)
- Performance notes
- Architecture deep dive
Installation
yarn add react-native-reanimated-carousel
# or
npm install react-native-reanimated-carouselPeer dependencies (see the Version Compatibility table above for exact ranges):
react-native-reanimatedreact-native-gesture-handlerreact-native-worklets(v5 only)
Configure Reanimated's Babel plugin and wrap your app in <GestureHandlerRootView> per the docs of those libraries. The carousel mounts its own GestureHandlerRootView internally, but you still need the top-level one for gesture handler to work.
Quick Start
import Carousel from "react-native-reanimated-carousel";
import { View, Text, useWindowDimensions } from "react-native";
export function Example() {
const { width } = useWindowDimensions();
return (
<Carousel
data={["Slide 1", "Slide 2", "Slide 3", "Slide 4"]}
style={{ width, height: 220 }}
loop
pagingEnabled
renderItem={({ item, index }) => (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>{item} (#{index})</Text>
</View>
)}
/>
);
}Every prop is optional except data and renderItem. The container fills its parent if you omit style (with a dev warning), so for production usage always declare a width and height via style.
Sizing model: container vs. page
The carousel distinguishes container size from page size:
- Container size is the outer viewport. It comes from
style, falling back to deprecatedwidth/heightprops, and finally toonLayoutmeasurement. - Page size is the snap distance and the unit of animation progress (one full page = animation
valuemoving by 1.0). It defaults to the container size — meaning one item fills the viewport — but you can override it withitemWidth/itemHeightto show multiple items at once, or withgetItemWidth/getItemHeightfor per-item sizes.
// Container 360 px wide, three items visible per page (120 px each)
<Carousel
style={{ width: 360, height: 200 }}
itemWidth={120}
data={data}
renderItem={renderItem}
/>Vertical carousels use the same model, swapping width ↔ height and itemWidth ↔ itemHeight:
<Carousel
vertical
style={{ width: 240, height: 600 }}
itemHeight={150}
data={data}
renderItem={renderItem}
/>Variable-size mode (getItemWidth / getItemHeight)
For long lists where every item has a different size — chat feeds, image grids with mixed aspect ratios, masonry-style content — supply getItemWidth(index) (horizontal) or getItemHeight(index) (vertical). The carousel precomputes a prefix-sum offset table before mount, so every visible item is positioned correctly on the first frame with no measurement race, no blank frames, no jump-on-measure.
const WIDTHS = [80, 150, 220, 100, 300, 100, 120, 200, 90, 180];
<Carousel
data={items}
style={{ width: screenWidth, height: 200 }}
getItemWidth={(index) => WIDTHS[index % WIDTHS.length]}
windowSize={10}
renderItem={renderItem}
/>Requirements & caveats
- The function must return a positive, finite number for every index in
[0, data.length). Non-finite or non-positive values are clamped to 0 (and warned about in__DEV__). - Treat the function identity as part of the data identity. Memoize with
useCallbackor hoist to module scope — re-creating it on every render triggers a full prefix-sum rebuild. (O(n) on the JS thread; fast for thousands of items but worth avoiding.) onLayoutmeasurements never overridegetItem*values. Your declarations are authoritative. If your real content is bigger than declared, it overflows; if smaller, you get trailing whitespace inside the item slot.pagingEnabledsnaps by item index — one swipe advances one item, regardless of width. Per-snap pixel distance therefore varies.autoFillDatais disabled in variable-size mode. autoFillData duplicates short data arrays assuming uniform size; with variable sizes the heuristic doesn't apply. Pre-duplicate your data array manually if you need a short loopable list.loopis fully supported. Items wrap around using the total span of all declared sizes.- Lookup cost. Each visible item performs a binary search through the prefix table on every gesture frame (≈ log₂(N) comparisons per item). For lists with hundreds of items this is negligible; for tens of thousands the cached-hint optimization keeps amortized cost at O(1) during continuous scrolling.
When to use each sizing prop
| Prop | Use when |
| --- | --- |
| style={{ width, height }} only | One item per page, uniform size |
| itemWidth / itemHeight | Multiple items per page, all the same size |
| getItemWidth / getItemHeight | Per-item sizes are known up-front |
Animation modes
The mode prop selects a built-in animation strategy. All modes preserve the "no blanks" rendering guarantee — items render synchronously on the UI thread.
normal (default)
Pure horizontal/vertical scroll. Each item translates by the page size as it moves through the viewport.
<Carousel data={data} style={{ width: 300, height: 200 }} renderItem={renderItem} />parallax
Items scale down and translate inward as they leave the center, creating a depth effect.
<Carousel
mode="parallax"
modeConfig={{
parallaxScrollingOffset: 50,
parallaxScrollingScale: 0.9,
parallaxAdjacentItemScale: 0.75,
}}
data={data}
style={{ width: 300, height: 200 }}
renderItem={renderItem}
/>modeConfig options (all optional):
parallaxScrollingOffset(default100): pixels each adjacent item is inset by.parallaxScrollingScale(default0.8): scale of the centered item.parallaxAdjacentItemScale(defaultparallaxScrollingScale ** 2): scale of the off-center items.
horizontal-stack / vertical-stack
Tinder-style card stack. Items pile up on top of each other and animate off the stack as the user swipes.
<Carousel
mode="horizontal-stack"
modeConfig={{
snapDirection: "left", // or "right"
showLength: 3, // number of cards visible in the stack
stackInterval: 18,
scaleInterval: 0.04,
opacityInterval: 0.1,
rotateZDeg: 30,
moveSize: screenWidth,
}}
data={data}
style={{ width: 300, height: 400 }}
renderItem={renderItem}
/>Use vertical-stack with vertical flag for vertical card stacks.
customAnimation
Skip the built-in modes and define your own animation as a worklet. Receives the animation value (–1 = one item to the left of center, 0 = centered, 1 = one to the right) and the item index, returns a React Native ViewStyle.
import type { ViewStyle } from "react-native";
import { interpolate } from "react-native-reanimated";
const customAnimation = (value: number, index: number): ViewStyle => {
"worklet";
const translateX = interpolate(value, [-1, 0, 1], [-300, 0, 300]);
const opacity = interpolate(value, [-1, 0, 1], [0.5, 1, 0.5]);
return {
transform: [{ translateX }],
opacity,
};
};
<Carousel customAnimation={customAnimation} data={data} renderItem={renderItem} />The carousel sanitizes the returned style on every frame: non-finite zIndex values are clamped to integers, and unsupported keys are dropped.
Loop, paging, and snapping
These three props together describe the carousel's scroll behavior. They're independent:
| Prop | Default | Effect |
| --- | --- | --- |
| loop | true | When true, scrolling past the last item wraps to the first. When false, the carousel stops at the boundaries. |
| pagingEnabled | true | When true, releasing a swipe always snaps to the next or previous item boundary (one swipe = one item). When false, the carousel scrolls freely. |
| snapEnabled | true | When true and pagingEnabled is false, releasing a swipe snaps to the nearest item boundary. When both are false, the carousel comes to rest wherever the gesture decay ends. |
Tweaking snap behavior
maxScrollDistancePerSwipe— maximum pixel distance a single swipe can travel. Useful for preventing flicks from scrolling past several items at once.minScrollDistancePerSwipe— minimum pixel distance for a swipe to register. Below this threshold, the carousel resets to the current item.scrollAnimationDuration(default500ms) — how long the spring/timing animation takes when snapping.withAnimation— replace the default ease-out-quart timing with a custom timing or spring config. Takes precedence overscrollAnimationDuration.
<Carousel
withAnimation={{
type: "spring",
config: { damping: 13, mass: 1.2, stiffness: 100 },
}}
data={data}
renderItem={renderItem}
/>fixedDirection
Forces every swipe to move in a single direction regardless of which way the user drags. Useful for "always scroll forward" patterns.
<Carousel fixedDirection="positive" data={data} renderItem={renderItem} />Auto-play
<Carousel
autoPlay
autoPlayInterval={2500} // ms between slides (default 1000)
autoPlayReverse={false} // when true, plays backwards
data={data}
renderItem={renderItem}
/>Auto-play pauses on touch and resumes on release. It also pauses if enabled={false} is set.
Gestures and overscroll
enabled(defaulttrue) — whenfalse, the carousel ignores all gestures. Programmatic control (ref.scrollTo,next,prev) still works.overscrollEnabled(defaulttrue) — whenfalse, scrolling stops exactly at the edges in non-loop mode. Whentrue, you can drag past the edge with rubber-band feel.onConfigurePanGesture— receives the underlyingPanGestureso you can compose with other gesture handlers, setactiveOffsetX/activeOffsetY, or chain.simultaneousWithExternalGesture(...).
import { Gesture } from "react-native-gesture-handler";
<Carousel
onConfigurePanGesture={(pan) => {
pan.activeOffsetX([-10, 10]);
}}
data={data}
renderItem={renderItem}
/>Programmatic control (ref)
Attach a ref<ICarouselInstance> to drive the carousel from code.
import { useRef } from "react";
import Carousel, { type ICarouselInstance } from "react-native-reanimated-carousel";
const ref = useRef<ICarouselInstance>(null);
<Carousel ref={ref} data={data} renderItem={renderItem} />;
// Imperative methods:
ref.current?.next(); // advance by 1
ref.current?.next({ count: 3, animated: true }); // advance by 3
ref.current?.prev({ count: 2 }); // go back 2
ref.current?.scrollTo({ index: 0, animated: true }); // jump to index 0
ref.current?.scrollTo({ count: -5 }); // equivalent to prev(5)
ref.current?.getCurrentIndex(); // returns the current item indexTCarouselActionOptions
| Field | Type | Description |
| --- | --- | --- |
| index | number | Absolute target index. Takes precedence over count. |
| count | number | Relative offset (positive = forward, negative = backward). |
| animated | boolean | Whether to animate the scroll. Defaults to true for next/prev, false for scrollTo. |
| onFinished | () => void | Called when the animation settles (or immediately if animated: false). |
Tracking progress
Three callbacks fire during scrolling:
<Carousel
onScrollStart={() => console.log("scroll began")}
onScrollEnd={(index) => console.log("settled at", index)}
onSnapToItem={(index) => console.log("snapped to", index)}
onProgressChange={(offsetProgress, absoluteProgress) => {
// offsetProgress: total pixel offset from start (negative when scrolling forward)
// absoluteProgress: fractional item index (e.g. 1.5 = halfway between items 1 and 2)
}}
data={data}
renderItem={renderItem}
/>onProgressChange with a SharedValue
For tight integration with Reanimated worklets, pass a SharedValue<number> directly. The carousel writes absoluteProgress into it on every frame without ever crossing the JS thread:
import { useSharedValue } from "react-native-reanimated";
const progress = useSharedValue(0);
<Carousel onProgressChange={progress} data={data} renderItem={renderItem} />;This is the recommended pattern for driving a <Pagination> indicator (see below).
scrollOffsetValue (advanced)
Pass your own SharedValue<number> as the carousel's internal translation state. The carousel mutates it during gestures and animations, letting you observe or write to the scroll offset from outside the component. Use this when you need to synchronize the carousel with another animated component (custom indicator, parallax background, etc.).
The legacy defaultScrollOffsetValue prop is still accepted but deprecated; prefer scrollOffsetValue.
<Pagination> indicators
Two flavors, both driven by a SharedValue<number> (typically wired up via onProgressChange).
Pagination.Basic
Simple dots that interpolate width / color between active and inactive states.
import { Pagination } from "react-native-reanimated-carousel";
import { useSharedValue } from "react-native-reanimated";
const progress = useSharedValue(0);
<Carousel onProgressChange={progress} data={data} renderItem={renderItem} />
<Pagination.Basic
progress={progress}
data={data}
horizontal
size={12}
dotStyle={{ backgroundColor: "#999", borderRadius: 6 }}
activeDotStyle={{ backgroundColor: "#000" }}
containerStyle={{ gap: 8 }}
onPress={(i) => ref.current?.scrollTo({ index: i, animated: true })}
/>Pagination.Custom
Same API plus a customReanimatedStyle(progress, index, length) => style worklet so you can implement any indicator shape (bars, segments, fading rings, etc.).
<Pagination.Custom
progress={progress}
data={data}
size={20}
customReanimatedStyle={(progress, index, length) => {
"worklet";
const distance = Math.abs(progress - index);
return {
opacity: 1 - Math.min(distance, 1),
transform: [{ scale: 1 - 0.4 * Math.min(distance, 1) }],
};
}}
/>Accessibility overrides
Both variants accept paginationItemAccessibility(index, length) returning overrides:
<Pagination.Basic
progress={progress}
data={data}
carouselName="Featured photos"
paginationItemAccessibility={(index, length) => ({
accessibilityLabel: `Photo ${index + 1} of ${length}`,
accessibilityRole: "button",
})}
/>Without overrides, items announce as "Slide N of M" (or "Slide N of M - <carouselName>" if carouselName is set).
Render-item contract
type CarouselRenderItem<T> = (info: {
item: T;
index: number;
animationValue: SharedValue<number>;
}) => React.ReactElement;item— the data entry.index— the real item index (already corrected forautoFillDataduplication).animationValue— aSharedValue<number>representing the item's position relative to the viewport:0when centered,-1when one page to the left,1when one page to the right, and so on. Useful for driving per-item animations from inside the rendered subtree.
import Animated, { useAnimatedStyle, interpolate } from "react-native-reanimated";
const renderItem = ({ item, animationValue }) => {
const style = useAnimatedStyle(() => ({
opacity: interpolate(animationValue.value, [-1, 0, 1], [0.4, 1, 0.4]),
}));
return <Animated.View style={[styles.card, style]}>{/* ... */}</Animated.View>;
};Complete prop reference
All props on <Carousel> (everything except data and renderItem is optional).
Data & rendering
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| data | T[] | required | Items to render. |
| renderItem | CarouselRenderItem<T> | required | Function that returns a React element for each item. |
| autoFillData | boolean | true | When loop=true and data.length is 1 or 2, duplicates entries internally so the loop animation has enough items. Disabled in variable-size mode. |
| defaultIndex | number | 0 | Initial item index. |
| keyExtractor | implicit | uses index | Items are keyed by their array index. |
Sizing
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| style | StyleProp<ViewStyle> | — | Container style. Provide width/height here. |
| contentContainerStyle | StyleProp<ViewStyle> | — | Style for the inner content container. Avoid opacity and transform here — they conflict with internal animations. |
| itemWidth | number | container width | Horizontal page size (snap distance). Use to show multiple items at once. |
| itemHeight | number | container height | Vertical page size when vertical=true. |
| getItemWidth | (i: number) => number | — | Per-item width. Enables variable-size mode. |
| getItemHeight | (i: number) => number | — | Per-item height for vertical carousels. |
| vertical | boolean | false | When true, items scroll top-to-bottom. |
| width | number | — | Deprecated. Use style={{ width }}. |
| height | number | — | Deprecated. Use style={{ height }}. |
Scroll behavior
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| loop | boolean | true | Wrap-around scrolling. |
| pagingEnabled | boolean | true | Snap to one-item-at-a-time on release. |
| snapEnabled | boolean | true | When paging is off, still snap to the nearest item. |
| overscrollEnabled | boolean | true | Allow dragging past the edges (non-loop mode). |
| enabled | boolean | true | Disable all gesture input. |
| fixedDirection | "positive" \| "negative" | — | Force every swipe to move in the given direction. |
| maxScrollDistancePerSwipe | number | — | Cap on a single swipe's pixel distance. |
| minScrollDistancePerSwipe | number | — | Threshold below which a swipe is ignored. |
| scrollAnimationDuration | number | 500 | Animation duration for snap, in milliseconds. |
| withAnimation | WithSpringAnimation \| WithTimingAnimation | — | Custom animation config; takes precedence over scrollAnimationDuration. |
| windowSize | number | data.length | Number of items rendered around the visible window. Lower = better perf with very long lists. |
Auto-play
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| autoPlay | boolean | false | Automatically advance. |
| autoPlayInterval | number | 1000 | Milliseconds between auto-advances. |
| autoPlayReverse | boolean | false | Play backwards. |
Animation modes
| Prop | Type | Description |
| --- | --- | --- |
| mode | "parallax" \| "horizontal-stack" \| "vertical-stack" | Built-in animation strategy. Omit for the default linear translation. |
| modeConfig | ILayoutConfig (parallax) or stack ILayoutConfig | Mode-specific tuning. |
| customAnimation | (value: number, index: number) => ViewStyle | Worklet that returns the per-item style. Bypasses built-in modes. |
| customConfig | CustomConfig \| (() => CustomConfig) | Override the internal type/viewCount used by useOffsetX. Advanced. |
Callbacks
| Prop | Type | Description |
| --- | --- | --- |
| onScrollStart | () => void | Fires when a gesture begins or next/prev/scrollTo is called. |
| onScrollEnd | (index: number) => void | Fires when the scroll settles. |
| onSnapToItem | (index: number) => void | Fires when the carousel snaps to a new item. |
| onProgressChange | ((offset: number, abs: number) => void) \| SharedValue<number> | Continuous scroll progress. Pass a SharedValue for zero-overhead worklet integration. |
| onConfigurePanGesture | (pan: PanGesture) => void | Customize the underlying Gesture Handler PanGesture. |
| onLayout | (e: LayoutChangeEvent) => void | Standard React Native onLayout for the container. |
Shared values
| Prop | Type | Description |
| --- | --- | --- |
| scrollOffsetValue | SharedValue<number> | External translation state; the carousel mutates this during gestures. |
| defaultScrollOffsetValue | SharedValue<number> | Deprecated alias for scrollOffsetValue. |
Misc
| Prop | Type | Description |
| --- | --- | --- |
| testID | string | E2E test identifier. |
| ref | Ref<ICarouselInstance> | Imperative handle. See Programmatic control. |
TypeScript exports
import Carousel, {
Pagination,
type TCarouselProps,
type ICarouselInstance,
type CarouselRenderItem,
type IComputedDirectionTypes,
type TAnimationStyle,
type ILayoutConfig,
} from "react-native-reanimated-carousel";TCarouselProps<T>— full props shape, generic overdata[number].ICarouselInstance— the imperative handle (next,prev,scrollTo,getCurrentIndex).CarouselRenderItem<T>— therenderItemsignature.IComputedDirectionTypes— internal helper for the vertical/horizontal prop variants; rarely needed at the call site.TAnimationStyle— thecustomAnimationworklet signature.ILayoutConfig— the stack mode'smodeConfigshape.
Migration notes (v4 → v5)
- Sizing: move
width/heightintostyle. The props still work but emit deprecation warnings. - Page size:
itemWidth/itemHeightnow control snap distance, not container size. To show multiple items at once: keep the container at full width viastyle={{ width }}, and setitemWidthto the per-page distance. - Scroll offset: prefer
scrollOffsetValueover the deprecateddefaultScrollOffsetValue. - Progress:
onProgressChangeaccepts aSharedValue<number>directly. The two-arg callback form still works. - Custom animations: styles are now sanitized; non-finite
zIndexis normalized. If you were relying on unsupported style keys, they'll be silently dropped. - Pagination accessibility: both
Pagination.BasicandPagination.Customnow acceptpaginationItemAccessibilityandcarouselNameprops for screen-reader customization. - Variable-size mode (new in this release): if you previously emulated variable widths via
customAnimation, you can now usegetItemWidth/getItemHeightfor a fully integrated solution that preserves snap behavior, looping, and progress tracking.
Performance notes
- Synchronous rendering: every visible item's position is a pure function of
handlerOffsetevaluated on the UI thread. The carousel doesn't await measurement before placing items, which is what makes scrolling jank-free even on slow devices. windowSize: for lists longer than ~20 items, setwindowSizeto a small number (e.g., 5–10) so off-screen items are not even constructed.autoFillData: only kicks in fordata.length1 or 2 whenloop=true. For longer arrays it's a no-op; you don't need to disable it.- Variable-size lookups:
O(log n)per visible item per frame, with a cached-hint fast path for continuous scrolling that amortizes toO(1). For lists in the tens of thousands, this is still well below 1 ms per frame. customAnimation: every worklet runs on every frame for every visible item. Keep them simple — preferinterpolateover hand-rolled math, and avoid allocating arrays/objects inside the worklet.
Architecture deep dive
For contributors and curious users who want to understand the internals — the hooks ecosystem, the math behind position interpolation, the gesture state machine, and how the SizeResolver abstraction makes variable-size mode work — see docs/ARCHITECTURE.md.
