One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Pager

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

One.UI.Pager on iOS and Android, held mid swipe between two pages

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.

Usage

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.

How it works

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.

The ref’s methods are Fabric commands that run on the native view without a render.

API reference

One.UI.PagerTypeDescription
childrenReactNodeOne React Native element per page. Each child fills the pager. Anything other than an element throws.
ReactNodeOne 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 = 0The 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 = trueAllows 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 = 0Gap 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 = falseiOS 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.
numberAndroid 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.

EventsTypeDescription
onPageScroll(event: PagerScrollEvent) => voidEvery scroll frame during a drag or an animated settle, and once with offset 0 when a settle lands.
(event: PagerScrollEvent) => voidEvery 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) => voidOnce 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) => voidThe scroll state changes between idle, dragging, and settling.

Every event has the shape { nativeEvent }:

PagerScrollEventTypeDescription
positionnumberThe page at or before the current scroll position.
numberThe page at or before the current scroll position.
offsetnumberProgress from position toward the next page, from 0 up to 1.
numberProgress from position toward the next page, from 0 up to 1.
PagerSelectedEventTypeDescription
positionnumberThe selected page.
numberThe selected page.
PagerScrollStateEventTypeDescription
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.
PagerRefTypeDescription
setPage(index: number) => voidAnimates to a page. Fires onPageScrollStateChanged with settling, then idle.
(index: number) => voidAnimates to a page. Fires onPageScrollStateChanged with settling, then idle.
setPageWithoutAnimation(index: number) => voidJumps to a page with no animation.
(index: number) => voidJumps 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) => voidAllows 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 | 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.

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.

Platform differences

iOSAndroidWeb
Native viewPaging scroll viewViewPager2Scroll snap
Pages attachedAllOffscreen page limitAll
At the first and last pageBounces with overdragGlow, per overScrollModeScroll stays in the pager

Limits and gotchas

Examples

Move with buttons

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.

Animate page dots

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.

Migrating from react-native-pager-view

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.