Onboarding pager

Free · MITReact Native · Expo

A full-screen onboarding pager: pages follow the finger 1:1 with parallax on their content, the page dot stretches toward the next page like a worm, and the Next button grows with the swipe into a full-width Get started on the last page.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/page-dots-onboarding.json

Then run npx expo install --fix so react-native-gesture-handler, react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/page-dots-onboarding.

Usage

import { OnboardingPager } from "@/components/goodui-native/page-dots-onboarding/page-dots-onboarding";

export function Welcome({ finish }: { finish: () => Promise<void> }) {
  return (
    <OnboardingPager
      onDone={finish}
      pages={[
        { key: "book", title: "Book in two taps", description: "See open slots and confirm without a phone call." },
        { key: "remind", title: "Never miss a visit", description: "A reminder the day before and an hour before." },
        { key: "pay", title: "Pay when you leave", description: "Your card stays on file. Tips are optional." },
      ]}
    />
  );
}

When to use

Use it for
  • Two to six welcome pages before sign-up or on first launch: what the app does, why it matters, permissions to come.
  • A short 'what's new' sequence after a big update.
Reach for something else
  • Pointing at controls on a real screen: use coach-mark.
  • Browsing content cards inside a screen: use carousel.
  • Switching between views of the same content: use segmented-control or tab-bar.

Examples

Own layout with parallax layers

{
  key: "stats",
  title: "Track your progress",
  content: (
    <View style={{ flex: 1, justifyContent: "center", padding: 24, gap: 16 }}>
      <OnboardingParallax depth={-0.4} scale><StatsArt /></OnboardingParallax>
      <OnboardingParallax depth={0.2}><Text size="title1" weight="medium">Track your progress</Text></OnboardingParallax>
    </View>
  ),
}

Skip straight out, localized

<OnboardingPager pages={pages} onDone={finish} onSkip={finish} labels={{ next: "Siguiente", done: "Empezar", skip: "Omitir" }} />

API reference

OnboardingPager

PropTypeDefault
pages*
The pages, in order. Built for two to six.
OnboardingPage[]—
onDone*
Runs when the last page's button is pressed. Return a promise and the button shows a spinner until it settles.
() => unknown—
index
Controlled page index. Pair with `onIndexChange`.
number—
defaultIndex
Uncontrolled first page. Default 0.
number0
onIndexChange
Called when a different page settles.
(index: number) => void—
skippable
Show the Skip button at the top until the last page. Default `true`.
booleantrue
onSkip
Called by Skip. Defaults to moving to the last page.
() => void—
labels
Overrides for the built-in strings (buttons and screen reader text); a GooduiLocaleProvider pack sets them app-wide.
Partial<OnboardingPagerLabels>—
parallax
Strength of the parallax on page content, 0 to 2. Default 1.
number1
haptic
Select haptic per page, tap on the buttons. Default `true`.
booleantrue
accessibilityLabel
Name of the pager for screen readers. Default "Onboarding". Same as `labels.pager`.
string—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—

OnboardingParallax

PropTypeDefault
depth
How far this layer moves on top of the page as a fraction of the page width. Negative lags (background), positive leads. Default 0.2.
number0.2
fade
Fade the layer as its page leaves the center. Default `true`.
booleantrue
scale
Shrink the layer a little as its page leaves the center. Default `false`.
booleanfalse
children*ReactNode—
style
Style for the outer container.
StyleProp<ViewStyle>—

Motion and haptics

  • Pages follow the finger 1:1 with rubber-band resistance past the ends and settle on the smooth spring with the release velocity, at most one page per swipe.
  • Parallax: the default illustration lags the page (depth −0.3) and shrinks a little, the title leads slightly (0.12) and the description leads more (0.32), so text arrives staggered; all fade with distance from the center.
  • Page dots: the accent worm's leading edge reaches the next dot in the first half of the swipe and its trailing edge catches up in the second half.
  • The Next button's width follows the swipe from compact to the full footer width between the last two pages, while Next slides out and Get started slides in and the dots and Skip fade away.
  • Reduced motion: buttons and dots jump straight to the page and the layers stay still (they only fade); dragging still follows the finger.
  • Haptic: select when a different page settles, tap on Next, Get started, Skip; error if onDone's promise rejects. haptic={false} turns them off.
  • One shared position drives the track, every parallax layer, the worm and the button on the UI thread; nothing re-renders while swiping.
  • All pages stay mounted; keep illustrations light (svg or optimized images).

Accessibility

  • The page dots are one adjustable element: swipe up or down on them with VoiceOver or TalkBack to change pages; its value reads 'Page N of M', the title and the description. The pages themselves stay readable, so buttons inside custom pages are reachable.
  • On the web the dots are a focusable slider with aria-valuenow, min, max and valuetext; the left and right arrow keys change pages. The pages are not inside it, so their own buttons are never nested in the slider.
  • Only the current page is reachable: pages off screen are inert on the web and hidden from VoiceOver and TalkBack on native, so Tab and swipe navigation never land on an invisible page.
  • Page changes are announced. The Next button is labelled Next or Get started (the hidden label is aria-hidden), busy while onDone's promise is pending.
  • Skip is a button that disappears from screen readers on the last page. Each dot is a tap target for touch and the mouse; screen readers and the keyboard use the slider.

Notes for AI

  • Give it a full-screen container (flex: 1); it lays out Skip at the top, the pages, then dots and the button at the bottom.
  • onDone runs on the last page's button; return a promise (for example saving a 'seen onboarding' flag) to show a spinner.
  • Without onSkip, Skip moves to the last page; pass onSkip={finish} to leave right away.
  • Use content for a custom page and wrap layers in OnboardingParallax; depth is a fraction of the page width (negative lags).