Skip to content
Native Animation
Page Curl Reader animation preview

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

npx nativeanimation add page-curl-reader
CarouselSkia

Page Curl Reader

A reader with touch-driven page curls and configurable pages, labels, paper color, 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 reader with touch-driven page curls and configurable pages, labels, paper color, and fonts.

How it works

  1. All page sources are preloaded on mount. Expo Image previews stay mounted with transition={0} and cachePolicy="memory"; all decoded Skia textures stay mounted in fixed layers. Animated navigation becomes available once all textures are ready, so the first turn never reveals an empty page.
  2. One shared curl snapshot contains progress, bend position, logical direction, and the displayed page index. The index and progress reset atomically without changing any image source. Two shared worklet meshes derive the front and reverse once per frame, independent of the number of mounted pages. Inactive pages use empty clips. Textures use normal source-over compositing; a color matrix provides lighting without multiplying or clearing the illustration below.
  3. Gesture Handler owns the outer-edge pan and the accessible tap alternative. Release velocity chooses a commit or a reversible cancel.
  4. The next or previous image sits below the moving sheet. A narrow contact shadow follows the fold; shadow and paper lighting follow a continuous zero-to-peak-to-zero envelope. Native page numbers, titles, and captions switch together at 70% progress, reversing naturally with a canceled gesture and never overlapping or leaving an empty caption. All caption layers stay mounted and reserve the tallest caption height to prevent layout jumps.
  5. Discrete page changes are reported after the turn settles; large jumps use a short fade and never sweep through unrelated pages.

Install

Copy this folder into your project, excluding demo/.

npx expo install @shopify/react-native-skia expo-image react-native-gesture-handler react-native-reanimated react-native-worklets

Keep the Skia web setup used by your Expo application and mount the component below a GestureHandlerRootView.

Usage

import PageCurlReader from './page-curl-reader';

export function Herbarium() {
  return <PageCurlReader pages={pages} />;
}

With a controlled index, custom theme, and a ref:

const reader = useRef<PageCurlReaderHandle>(null);
return (
  <PageCurlReader
    ref={reader}
    pages={pages}
    colorScheme="dark"
    curlRadiusFraction={0.14}
    onIndexChange={setIndex}
  />
);

Props

PropTypeDefaultDescription
pagesReadonlyArray<PageCurlReaderPage>requiredLocal page artwork, optional inline preview URI, and native title/caption content.
indexnumberuncontrolledControlled zero-based page index.
defaultIndexnumber0Initial uncontrolled index.
onIndexChange(index: number) => void—Called once after a confirmed turn.
onReadyChange(ready: boolean) => void—Reports whether all textures are available for animated navigation.
onPagePress(id: string) => void—Called when the current page is tapped.
disabledbooleanfalseDisables gestures, buttons, and imperative navigation; reset remains available.
paperColorstringtheme paperColor of the unprinted reverse.
curlRadiusFractionnumber0.12Fold radius, clamped to 0.08–0.18 of page width.
labelsPageCurlReaderLabelsneutral English labelsPrevious, next, page, empty, load-error, and retry copy.
colorScheme'light' | 'dark' | 'auto''auto'Complete Paper/Night theme selection.
palettePartial<Palette>theme tokensOverride any complete palette token.
fontFamily / titleFontFamily / monoFontFamilystringsystem fontsText, title, and counter typefaces.
motion{ durationScale?: number; glowOpacity?: number }—Duration scale defaults to 1 and clamps to 0.5–2; glowOpacity is accepted by the type but currently unused.
accessibilityLabelstringundefinedFallback accessible plate description when no page label is available; otherwise the page label takes precedence.
refReact.Ref<PageCurlReaderHandle>undefinedPublic navigation handle described below.
styleStyleProp<ViewStyle>—Root layout override.
testIDstring'page-curl-reader'Root test identifier; controls add -previous, -next, and -retry.

The handle exposes next(), previous(), goTo(index), and reset(). Empty or duplicate IDs are safely filtered; invalid indices clamp to the available page range. A failed image load shows the supplied retry action.

The demo waits for readiness, starts its first curl after 600 ms, and then turns every 2.5 seconds through all four pages and back in a continuous loop. Replay resets the reader and starts a new curl. Paper and Night themes cover the whole demo, including its header and footer. The reader fits its artwork into the remaining height without an internal scroll view.

Compatibility

Built for Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, Worklets 0.13.0, Gesture Handler 3.3, and Skia 2.14.0. The component owns no safe-area insets.

Accessibility

The page exposes its title, caption, page value, busy state, and increment/decrement actions. Previous and Next buttons are always available as a non-gesture alternative, and page presses have an accessible activation path. Reduced motion removes the deforming gesture and uses a short fade for changes. All visible copy is supplied by props.

Why this one

Page Curl Reader makes the paper itself the interaction surface: the bend, reverse, next plate, and contact shadow all respond to one interruptible gesture while editorial text remains native and adaptable.

Rendering details

Page textures use normal source-over drawing. Inactive pages have empty clips, and a color matrix provides lighting without multiplying the illustration or clearing the destination. The transparent book background keeps the mounted Expo Image preview visible while the first Skia frame initializes.

All image sources and caption layers remain mounted throughout a turn. Only the UI-thread curl snapshot changes during the gesture; React receives the discrete page index after completion. The outgoing front carries its original illustration until it folds out of view, and the revealed page carries the neighboring illustration throughout. The unprinted paper reverse remains intentionally plain.

Dispatch Next without advancing time, then capture ten screenshots at 80 ms intervals (0–720 ms) using page.clock.runFor(80). This samples the resting page, moving front, paper reverse, revealed neighbor, and final settling period. Check both illustration and caption in every screenshot; run the same sequence backward and check the autoplay loop separately. Virtual-clock screenshots verify rendering continuity, not physical-device frame rate.

Screenshots at 390×844 and 320×568 kept the caption, controls, and viewer close button clear. The transparent book background also keeps the mounted Expo Image preview visible while the first Skia frame initializes. No new artwork was needed.