Swipe between React Native pages on iOS, Android, and web

One.UI.Pager shows one child view per page and swipes between them, for
onboarding flows, image galleries, and tabbed content you swipe through. It is
a paging UIScrollView on iOS, Jetpack ViewPager2 on Android, and a CSS
scroll snap container on web.
Every page stays mounted in the React tree, so state and context survive a
swipe. The props, events, and ref match react-native-pager-view. For
SwiftUI pages inside a SwiftUI layout, use
One.iOS.Pager instead.
import { Text, View } from 'react-native'import { One } from 'one'
function Welcome() { return ( <One.UI.Pager style={{ flex: 1 }}> <View key="hello"><Text>Hello</Text></View> <View key="sync"><Text>Sync your notes</Text></View> <View key="done"><Text>All set</Text></View> </One.UI.Pager> )}Each direct child is one page and fills the pager. Give the pager a size and
each page a stable key.
The pager clones each child with an absolute, full-size style and hands the pages to a native container. Its props are checked in JavaScript first, so a bad value throws during render on every platform.
UIScrollView with paging on.
The scroll view is one page plus pageMargin wide, so each page snaps into
view and the margin shows only mid swipe. Progress is the scroll offset
divided by that stride, sent on every scroll frame.ViewPager2 through a RecyclerView adapter
that never recycles them, so each page keeps its native views. Margins are a
page transformer. When pagers nest, the inner one keeps the swipe until it
can scroll no further in that direction, then hands it to the outer one.The ref’s methods are Fabric commands that run on the native view without a render.
| One.UI.Pager | Type | Description |
|---|---|---|
childrenReactNodeOne React Native element per page. Each child fills the pager. Anything other than an element throws. | ReactNode | One React Native element per page. Each child fills the pager. Anything other than an element throws. |
initialPagenumber = 0The page shown on mount. Later changes are ignored; use the ref to move. | number = 0 | The page shown on mount. Later changes are ignored; use the ref to move. |
scrollEnabledboolean = trueAllows user swipes. The ref methods still move pages when it is false. | boolean = true | Allows user swipes. The ref methods still move pages when it is false. |
orientation"horizontal" | "vertical" = "horizontal"The swipe axis. | "horizontal" | "vertical" = "horizontal" | The swipe axis. |
layoutDirection"ltr" | "rtl" = "ltr"Horizontal page order. Indices keep child order, so page 0 sits on the right in rtl. | "ltr" | "rtl" = "ltr" | Horizontal page order. Indices keep child order, so page 0 sits on the right in rtl. |
pageMarginnumber = 0Gap between pages, visible only during a swipe. Must be zero or more. | number = 0 | Gap between pages, visible only during a swipe. Must be zero or more. |
keyboardDismissMode"none" | "on-drag" = "none"With on-drag, starting a swipe dismisses the keyboard. | "none" | "on-drag" = "none" | With on-drag, starting a swipe dismisses the keyboard. |
overdragboolean = falseiOS only. Lets the first and last pages bounce past the edge. | boolean = false | iOS only. Lets the first and last pages bounce past the edge. |
offscreenPageLimitnumberAndroid only. Pages kept laid out on each side of the current page: -1 for the ViewPager2 default, or a positive integer. | number | Android only. Pages kept laid out on each side of the current page: -1 for the ViewPager2 default, or a positive integer. |
overScrollMode"auto" | "always" | "never" = "auto"Android only. The edge glow at the first and last pages. | "auto" | "always" | "never" = "auto" | Android only. The edge glow at the first and last pages. |
The pager also takes View props; style sizes it.
| Events | Type | Description |
|---|---|---|
onPageScroll(event: PagerScrollEvent) => voidEvery scroll frame during a drag or an animated settle, and once with offset 0 when a settle lands. | (event: PagerScrollEvent) => void | Every scroll frame during a drag or an animated settle, and once with offset 0 when a settle lands. |
onPageSelected(event: PagerSelectedEvent) => voidOnce on mount with the initial page, then whenever the selected page changes. | (event: PagerSelectedEvent) => void | Once on mount with the initial page, then whenever the selected page changes. |
onPageScrollStateChanged(event: PagerScrollStateEvent) => voidThe scroll state changes between idle, dragging, and settling. | (event: PagerScrollStateEvent) => void | The scroll state changes between idle, dragging, and settling. |
Every event has the shape { nativeEvent }:
| PagerScrollEvent | Type | Description |
|---|---|---|
positionnumberThe page at or before the current scroll position. | number | The page at or before the current scroll position. |
offsetnumberProgress from position toward the next page, from 0 up to 1. | number | Progress from position toward the next page, from 0 up to 1. |
| PagerSelectedEvent | Type | Description |
|---|---|---|
positionnumberThe selected page. | number | The selected page. |
| PagerScrollStateEvent | Type | Description |
|---|---|---|
pageScrollState"idle" | "dragging" | "settling"dragging while a finger moves the pager, settling while it animates to a page, idle once it rests. | "idle" | "dragging" | "settling" | dragging while a finger moves the pager, settling while it animates to a page, idle once it rests. |
| PagerRef | Type | Description |
|---|---|---|
setPage(index: number) => voidAnimates to a page. Fires onPageScrollStateChanged with settling, then idle. | (index: number) => void | Animates to a page. Fires onPageScrollStateChanged with settling, then idle. |
setPageWithoutAnimation(index: number) => voidJumps to a page with no animation. | (index: number) => void | Jumps to a page with no animation. |
setScrollEnabled(enabled: boolean) => voidAllows or blocks user swipes without a render. The next change to the scrollEnabled prop replaces it. | (enabled: boolean) => void | Allows or blocks user swipes without a render. The next change to the scrollEnabled prop replaces it. |
getNativeScrollRef() => HostInstance | HTMLElement | nullReturns the view that owns the pager's pan, as React Native's ScrollView does, so react-native-gesture-handler can wrap the pager in Gesture.Native() and relate other gestures to its swipe. | () => HostInstance | HTMLElement | null | Returns the view that owns the pager's pan, as React Native's ScrollView does, so react-native-gesture-handler can wrap the pager in Gesture.Native() and relate other gestures to its swipe. |
An index must be a nonnegative integer; anything else throws. An index past the last page does nothing. A call made before the pager lays out moves it to that page on first layout.
| iOS | Android | Web | |
|---|---|---|---|
| Native view | Paging scroll view | ViewPager2 | Scroll snap |
| Pages attached | All | Offscreen page limit | All |
| At the first and last page | Bounces with overdrag | Glow, per overScrollMode | Scroll stays in the pager |
View.initialPage is read once. Later changes are ignored; move with the
ref instead.onPageScroll already reports settles
as well as drags. Use it as the only writer of progress rather than also
setting progress from onPageSelected.import { useRef, useState } from 'react'import { Button, Text, View } from 'react-native'import { One, type PagerRef } from 'one'
function Onboarding({ onDone }: { onDone: () => void }) { const pager = useRef<PagerRef>(null) const [page, setPage] = useState(0) const last = 2
return ( <View style={{ flex: 1 }}> <One.UI.Pager ref={pager} style={{ flex: 1 }} onPageSelected={({ nativeEvent }) => setPage(nativeEvent.position)} > <View key="welcome"><Text>Welcome</Text></View> <View key="sync"><Text>Sync</Text></View> <View key="done"><Text>Done</Text></View> </One.UI.Pager> <Button title={page === last ? 'Start' : 'Next'} onPress={() => (page === last ? onDone() : pager.current?.setPage(page + 1))} /> </View> )}onPageSelected fires once on mount and after every swipe or setPage, so
page is right whether the user swipes or taps.
import { View } from 'react-native'import Animated, { useAnimatedStyle, useSharedValue, type SharedValue } from 'react-native-reanimated'import { One } from 'one'
function Dot({ index, progress }: { index: number; progress: SharedValue<number> }) { const style = useAnimatedStyle(() => ({ opacity: Math.max(0.3, 1 - Math.abs(progress.get() - index)), })) return <Animated.View style={[{ width: 8, height: 8, borderRadius: 4, backgroundColor: 'black' }, style]} />}
function Gallery({ photos }: { photos: string[] }) { const progress = useSharedValue(0) return ( <View> <One.UI.Pager style={{ height: 360 }} onPageScroll={({ nativeEvent }) => progress.set(nativeEvent.position + nativeEvent.offset)} > {photos.map((uri) => ( <View key={uri}><One.UI.Image source={{ uri }} style={{ flex: 1 }} /></View> ))} </One.UI.Pager> <View style={{ flexDirection: 'row', gap: 6, justifyContent: 'center' }}> {photos.map((uri, index) => <Dot key={uri} index={index} progress={progress} />)} </View> </View> )}position + offset is a continuous page number, such as 1.4 between the
second and third pages. Writing it to a shared value updates the dots on the
UI thread without a React render per frame.
Replace the default import and JSX with One.UI.Pager, and type the ref as
PagerRef from one. Props, events, and ref methods keep their names and
payloads.
Edit this page on GitHub.