Skip to content
Native Animation
Windowed Dots Pager animation preview

This preview uses fictional demo content. See the props below for the customization options available in this component.

npx nativeanimation add windowed-dots-pager

Windowed Dots Pager

A pagination rail with moving dot windows and expanding capsules, with configurable page count, visible count, palette, and fonts.

$9 for this animation

Add to your project

Copy the component and its assets into your app with the Native Animation CLI, or paste the agent prompt into Claude Code, Codex or Cursor once the MCP server is connected.

About this animation

A pagination rail with moving dot windows and expanding capsules, with configurable page count, visible count, palette, and fonts.

Why this one

Connect one shared value to any pager, without adopting an onboarding screen. A bounded pool of maxVisible + 2 dots handles 1, 5, 12, 40, or more pages with constant rendering cost. Neighboring capsules stretch toward one another, join, and separate while the window follows the current page. The demo uses twelve static geometric ritual tiles, not a second carousel.

How it works

  1. onLayout measures the component's own container. The odd window fits that width using 48-point slots.
  2. One continuous zero-based position controls capsule width, opacity, color, and the connecting neck. Endpoint dots taper to 4, 6, and 8 points; the active capsule rests at 28 × 8.
  3. Crossing half a page moves the window by one slot in 220 ms. A fixed pool recycles page identities on the UI thread.
  4. Gesture Handler composes pan and tap; each slot has its own non-overlapping 48-point region. Previous/Next provide separate 48-point alternatives.
  5. Requests call onIndexChange once per new destination. Observing host progress never emits callbacks. React updates only when the rounded page changes, not on every frame.
  6. Explicit-mass springs settle gestures. Outside the range, elastic resistance shifts the rail slightly. RTL mirrors slots and gestures while preserving page numbering.

Install

Copy this folder, excluding demo/, into your app. No assets, catalog imports, or font loading are required.

npx expo install react-native-gesture-handler react-native-reanimated react-native-worklets

Mount your application inside GestureHandlerRootView. Demo-only dependencies are expo-image and expo-linear-gradient. Icons: Lucide (ISC); the Aurora sparkles path is copied from the project's kit.

Usage

import { WindowedDotsPager } from './windowed-dots-pager';

<WindowedDotsPager
  count={12}
  defaultIndex={2}
  accessibilityLabel="Page navigation"
  onIndexChange={(page) => selectPage(page)}
/>

Continuous host integration and personality knobs:

const progress = useSharedValue(2);
const pager = useRef<WindowedDotsPagerRef>(null);

<WindowedDotsPager
  ref={pager}
  count={40}
  progress={progress}
  maxVisible={5}
  colorScheme="dark"
  fontFamily="YourInterfaceFont"
  monoFontFamily="YourNumberFont"
  accessibilityLabel="Journal pages"
  previousLabel="Back"
  nextLabel="Continue"
  formatPage={(index, count) => `${index + 1} / ${count}`}
  onIndexChange={(index) => {
    progress.set(withSpring(index, { damping: 26, stiffness: 300, mass: 1 }));
  }}
/>

Your host can write progress continuously. In progress mode, the rail also writes this shared value during a direct drag and springs it to a page on release. Refs and buttons only request a destination through the callback; your host applies it. Supply index for strictly controlled selection: it takes precedence over progress, and drag requests wait for the host's new index. Do not animate a separate host pager against the same shared value during an active rail gesture. Use onInteractionStart to stop host autoplay.

Props

PropTypeDefaultPurpose
countnumberrequiredNumber of pages; finite values are floored.
indexnumberuncontrolledControlled zero-based destination; highest priority.
defaultIndexnumber0Initial index and reset destination.
progressSharedValue<number>localContinuous position; writable during rail gestures unless index is supplied.
maxVisiblenumber5Normalized to odd ≥3, then limited by measured width and count.
onIndexChange(index: number) => void—Distinct destination request; not a frame callback.
formatPage(index, count) => string03 / 12 formatCounter and accessibility announcements; zero-based input.
previousLabel, nextLabelstringPrevious, NextVisible button text and accessible names.
statusLabels{ empty, invalid, hint: string }generic EnglishEmpty count message, invalid count message, and adjustable rail hint. Supply all three keys.
accessibilityLabelstringrequiredAccessible name of the rail.
colorScheme'light' | 'dark' | 'auto'autoComplete Night/Paper themes.
palettePagerPalettethemeFull { background, surface, text, muted, border, accent, success, error } override. Accent should be a six-digit hex color for halo alpha.
fontFamilystringsystemButton font.
titleFontFamilystringsystemEmpty/error status font.
monoFontFamilystringsystemCounter font.
onInteractionStart() => void—Real input notification for canceling host timers.
rtlbooleanI18nManager.isRTLMirror visual and gesture direction.
styleStyleProp<ViewStyle>—Root override; root defaults to flex: 1.
testIDstringwindowed-dots-pagerRoot ID; stable -track, -counter, -previous, -next suffixes.

ref: goTo(index), next(), previous(), reset(). All indices are zero-based. Repeated requests for the same destination do not duplicate callbacks. Uncontrolled successive ref calls follow the last requested destination. Controlled hosts should acknowledge requests through index or progress.

No labels object is needed: button copy is supplied through the two label props; all other component strings live in statusLabels and formatPage. The demo passes its copy explicitly and owns its autoplay timers (5-second loop, 2→5 in 600 ms, then reset in 300 ms). Touch cancels the loop; unmount clears timers.

Edge cases and layout

count=0 hides the rail and disables both actions. Negative/non-finite counts show the supplied accessible invalid message. Non-finite page values resolve to zero; finite indices clamp to the available range. count=1 disables navigation and has no animated halo. There are no item IDs to resolve. Changing count clamps display and requests without emitting a synthetic callback.

The component measures its own width, applies no safe area, and lays out its rail, counter, and actions without an internal scroll view. Allow at least 144 points of height with the default labels and text size; longer labels and larger text can require more space. The demo has a 480-point maximum column. It uses a compact six-column ritual grid on shorter screens, keeps theme and page-count controls in the same composition, and omits secondary copy. At heights below 650 points, one full-screen outer scroll view keeps the composition accessible with both scroll indicators hidden. Hosts apply safe areas once. Targets are at least 48 × 48; individual decorative dots are represented by one adjustable accessible rail.

Accessibility

Buttons have roles, labels, disabled/selected states, and stable IDs. The rail exposes increment/decrement actions, current/minimum/maximum values, and a customizable hint. Requests announce the formatted page. Reduced motion removes travel, neck stretching, glow, and press scaling; capsule emphasis fades over 200 ms. Only one counter and one ritual label are rendered, so old/new labels never overlap.

Compatibility

Built for Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, Gesture Handler 3.3, and Worklets 0.13.0. No additional native dependency or development build is needed beyond this installed Expo Go stack. The reusable component does not require Skia.

The compact demo uses one full-screen outer scroll view below 650 points with hidden indicators.