ScrollCanvas

An image in a still frame that parallaxes, zooms, reveals or scrubs a frame sequence as the page scrolls.

Open Source

Installation

ScrollCanvas ships with the library — install it once, then import:

import { ScrollCanvas } from "@delacour/react-native-ui/scroll-canvas";

Or copy the source into your project, to own and edit it:

bunx delacour@latest add scroll-canvas

Usage

Inside a Screen there is nothing to wire: the screen is the scroll source. Put the canvas directly in the scroll content.

app/article.tsx
import { Screen } from "@/components/ui/screen";
import { ScrollCanvas } from "@/components/ui/scroll-canvas";
import { Text } from "@/components/ui/text";

export default function Article() {
  return (
    <Screen>
      <Screen.ScrollArea>
        <ScrollCanvas accessibilityLabel="Fog over a mountain ridge" source={require("./ridge.jpg")} />
        <Text.Paragraph>The track drops into beech forest after the saddle.</Text.Paragraph>
      </Screen.ScrollArea>
    </Screen>
  );
}

Your own scrollable

Outside a Screen, make a source for the scrollable with useScrollSource and spread its props onto it. Nothing clones the scroll view for you.

import { useScrollSource } from "@/hooks/use-scroll-source";
import Animated from "react-native-reanimated";

function Story() {
  const { source, scrollProps } = useScrollSource();

  return (
    <Animated.ScrollView {...scrollProps}>
      <ScrollCanvas scrollSource={source} source={hero} />
    </Animated.ScrollView>
  );
}

Effects

The frame is the layout and never moves on its own; only the picture inside it does. Every effect moves by transform alone, on the UI thread, so nothing re-lays out mid-scroll.

Parallax

The default. The picture drifts against the scroll, so it seems to sit further away than the page. It is oversized by distance (a share of the frame height, each way) and never shows a frame edge at either end of its travel. It runs for the frame's whole time on screen.

Parallax
<ScrollCanvas source={hero} />
<ScrollCanvas distance={0.35} source={hero} />

Zoom

The picture enters oversized by distance and settles to its natural size by the time the frame's top is 30% down the view.

Zoom
<ScrollCanvas effect="zoom" distance={0.3} source={hero} />

Reveal

A cover in the page's colour retreats upward as the frame rises, uncovering the picture from its bottom edge. coverClassName restyles it — bg-card inside a card, say.

Reveal
<ScrollCanvas effect="reveal" source={hero} />

Sequence

Scroll position picks which of N frames is showing — a product turning, a diagram assembling. Each band of progress is one frame, with no cross-fade, so a slow drag scrubs and stops on a frame. At rest above its window it shows frame 0: make that the poster.

Sequence
<ScrollCanvas effect="sequence" sources={spinFrames} />

Every frame of a sequence is mounted at once, so it is memory, not bandwidth, that runs out. Keep it to 20–30 frames, exported at the size the canvas draws them rather than at full resolution. More than 30 warns in development.

The window

start and end say where the effect runs, in viewport terms: where the frame's top sits when it begins and when it is complete. 1 is the bottom edge, 0 the top, and a negative value is past the top. Every effect starts at 1, as the frame enters. Parallax ends as the frame leaves; the others end at 0.3. Set them equal for a step.

<ScrollCanvas effect="zoom" start={0.9} end={0.6} source={hero} />

To drive an effect from something other than the scroll — a pager's position, a gesture — pass a progress shared value from 0 to 1 instead. Nothing is measured, and no scroll source is needed.

Where the frame is

The effect needs the frame's top in the scroll content. How it finds it depends on the source:

SourceMeasured byHolds when
useScrollSourcemeasureLayout against the scroller, re-run when the content size changesalways — the canvas may be nested at any depth
A Screenthe frame's own onLayoutthe canvas is a direct child of the scroll content
progressnothingalways

Inside a Screen, wrapping a canvas in a card or a View offsets its window by the wrapper's top. Pass a useScrollSource source or progress there instead.

Reduced motion

A picture moving independently of the finger is a vestibular trigger, so with Reduce Motion on — or DelacourProvider's isMotionCalm, or enabled={false} — every effect draws its resting state: parallax centred, zoom at natural size, reveal uncovered, a sequence on frame 0. Nothing shifts when the setting flips. A canvas with enabled={false} needs no scroll source at all.

Resting states
Resting states

Accessibility

Give the canvas an accessibilityLabel and it is one image to a screen reader — a sequence is announced once, never per frame. Leave it off and the canvas is decorative and hidden. A sequence should carry nothing a single frame does not: the motion is decoration.

API Reference

ScrollCanvas

Prop

Type

On this page