In a React Native horizontal ScrollView, when do you use pagingEnabled, snapToInterval or snapToOffsets to build a snapping carousel?
answer
- pages the size of the scroll view
- fixed card width plus the gap
- variable widths need explicit offsets
- decelerationRate fast, snapToAlignment
- offsets override interval override paging
basics
~20 spagingEnabled stops on multiples of the scroll view's own size, so it suits full-width pages. snapToInterval stops on multiples of a value such as card width plus gap, and snapToOffsets stops at explicit offsets for cards of different widths.
solid answer
~40 s`pagingEnabled` makes the scroll view stop on multiples of its own width, which is right for full-screen onboarding pages but wrong for cards narrower than the screen. For an "episodes from this show" row of equal cards, set `snapToInterval` to the card width plus the gap, usually with `decelerationRate="fast"` and `snapToAlignment` (`'start'` by default, or `'center'`/`'end'`). When cards have different widths, pass `snapToOffsets` with the exact stopping points; `snapToStart` and `snapToEnd` (both `true` by default) also count the edges as snap points. The props override each other in that order: `snapToOffsets` beats `snapToInterval`, which beats `pagingEnabled`. `disableIntervalMomentum` makes a hard fling stop at the next item instead of skipping several. `FlatList` passes all of these through to its underlying `ScrollView`.
code
tsx · 31 linesimport { FlatList, StyleSheet, Text, View } from 'react-native';
const CARD_WIDTH = 280;
const GAP = 12;
type Episode = { id: string; title: string };
export function MoreFromThisShow({ episodes }: { episodes: Episode[] }) {
return (
<FlatList
horizontal
data={episodes}
keyExtractor={(e) => e.id}
showsHorizontalScrollIndicator={false}
snapToInterval={CARD_WIDTH + GAP}
snapToAlignment="start"
decelerationRate="fast"
disableIntervalMomentum
ItemSeparatorComponent={() => <View style={{ width: GAP }} />}
renderItem={({ item }) => (
<View style={styles.card}>
<Text numberOfLines={2}>{item.title}</Text>
</View>
)}
/>
);
}
const styles = StyleSheet.create({
card: { width: CARD_WIDTH, height: 160, padding: 12, backgroundColor: 'white' },
});go deeper
Recall what each prop stops on: the scroll view's size, a fixed interval, or listed offsets, and pair snapToInterval with decelerationRate fast.
Explain the override order, snapToAlignment and disableIntervalMomentum, and how padding or separators shift the snap grid.
Choose between the props for a real design, verify behaviour on both platforms, and know when a carousel needs scroll-linked animation on top of snapping.
Decide whether carousels use the built-in snapping props or a shared component, and keep their feel consistent across the app.
## Three ways to make a scroll view stop on purpose A normal React Native `ScrollView` decelerates and stops wherever momentum runs out. A carousel needs it to stop at meaningful positions. `ScrollView` has three props for that, and because `FlatList` renders a `ScrollView`, the same props work on a horizontal `FlatList` too. | Prop | Stops at | Best for | |---|---|---| | `pagingEnabled` | Multiples of the scroll view's own size | Full-width pages: onboarding, a photo viewer | | `snapToInterval` | Multiples of the number you pass | Equal-width cards narrower than the screen | | `snapToOffsets` | The exact offsets in the array you pass | Cards of different widths, or custom stops | ## `pagingEnabled` `pagingEnabled` (default `false`) snaps to multiples of the scroll view's width for horizontal scrolling. It is the simplest option and it feels native, but a page is always the whole viewport. In a podcast app's "More from this show" row, where each episode card is 280 points wide and you want the next card peeking in, paging would jump a full screen width and leave cards half visible. ## `snapToInterval` `snapToInterval={292}` stops at 0, 292, 584 and so on: card width plus the gap between cards. The docs pair it with two helpers: - **`snapToAlignment`**: `'start'` (default) aligns the snap to the left edge in a horizontal scroll view, `'center'` to the middle and `'end'` to the right edge. It only has meaning when `snapToInterval` is set. - **`decelerationRate="fast"`**: the scroll slows quickly after release, so it settles on a nearby snap point rather than coasting past many. The string values map to platform constants: `'normal'` is 0.998 on iOS and 0.985 on Android, `'fast'` is 0.99 on iOS and 0.9 on Android. You can also pass a number. Related props: - **`disableIntervalMomentum`** (default `false`): when `true`, the view stops at the next snap point relative to where the finger lifted, however hard the fling. Good for "one card per swipe" carousels. - **`disableScrollViewPanResponder`**: disables the scroll view's default JS pan responder; the docs call it useful with `snapToInterval`, and warn against it on ordinary scroll views. Content padding shifts the grid: if `contentContainerStyle` has a horizontal padding, the first card no longer starts at offset 0, so account for it in the interval or switch to explicit offsets. ## `snapToOffsets` When cards differ in width, for example a wide "latest episode" card followed by normal ones, no single interval fits. `snapToOffsets={[0, 340, 632, 924]}` lists the exact stops. Two companions control the edges: 1. `snapToStart` (default `true`): the beginning of the list counts as a snap point. Set it to `false` to scroll freely between the start and the first offset. 2. `snapToEnd` (default `true`): the same for the end of the content. ## Which prop wins The docs define an override order. `snapToOffsets` overrides both `snapToInterval` and `pagingEnabled`, and `snapToInterval` overrides `pagingEnabled`. Setting several is legal, so a stale `pagingEnabled` left in a component is silently ignored once `snapToInterval` is added. ## Choosing, in order 1. Is every page exactly the viewport width? Use `pagingEnabled`. 2. Are the cards equal width? Use `snapToInterval` with `decelerationRate="fast"`. 3. Otherwise compute `snapToOffsets` from the item widths. 4. Add `disableIntervalMomentum` if a fling must advance by exactly one item. ## Common mistakes - **Using `pagingEnabled` for peeking cards.** It pages by the viewport, so cards narrower than the screen end up cut in half after each swipe. - **Forgetting the gap in the interval.** `snapToInterval={CARD_WIDTH}` drifts by one gap per card; the interval is the distance from one card's start to the next. - **Setting `snapToAlignment` without `snapToInterval`.** It has no effect on its own. - **Leaving the default deceleration.** With `'normal'` a fling can coast past several snap points before settling, which feels loose in a carousel. - **Expecting `snapToOffsets` to ignore the edges.** The start and end count as snap points unless `snapToStart` or `snapToEnd` is set to `false`. ## When these props are not enough These props decide where scrolling stops. A carousel that also scales or fades cards as they approach the centre is a scroll-linked effect on top of the snapping: drive it from the scroll offset on the UI thread with a native-driven `Animated.event` or Reanimated's `useAnimatedScrollHandler`, not from `setState` in `onScroll`.
- You added snapToInterval to a carousel that already had pagingEnabled. Which one takes effect?`snapToInterval`. React Native's docs describe it as overriding the less configurable `pagingEnabled`, and `snapToOffsets` in turn overrides both. The leftover `pagingEnabled` is simply ignored, so remove it to avoid confusing the next reader.
- Cards stop slightly off-centre after you add horizontal padding to contentContainerStyle. Why?`snapToInterval` snaps to multiples of the interval measured from offset 0, but padding shifts where the first card begins. Either fold the padding into your layout so cards still start on multiples of the interval, or compute `snapToOffsets` from the real card positions.
saying these in an interview costs you the question
- pagingEnabled pages by the width of each child, not the scroll view.
- snapToAlignment works on its own without snapToInterval.
- snapToInterval overrides snapToOffsets when both are set.
- decelerationRate="fast" is what makes the view snap at all.
- FlatList does not support snapToInterval because it is virtualized.