Turn the scrollbar into an animation timeline. Two tiny, zero-config packages for building Apple-style scroll experiences in React.
npm i react-scroll-flip-book
npm i use-window-scroll-in-element
A primitive hook that reports exactly where the viewport sits inside any element — as raw pixels and as a 0 → 1 fraction. Bind that one number to width, opacity, transform… anything.
Get both the pixel offset and a normalized 0–1 progress value for the top and bottom edges.
Choose whether tracking begins and ends at the window top, center, or bottom.
A tiny throttled scroll listener. No context, no providers, no CSS to import.
npm i use-window-scroll-in-element
import { useRef } from 'react'; import { useWindowScrollInElement } from 'use-window-scroll-in-element'; function Showcase() { const ref = useRef(null); // fraction.top goes from 0 -> 1 as the element // scrolls through the viewport. const { fraction } = useWindowScrollInElement(ref, { scrollStartPosition: 'window-bottom', scrollEndPosition: 'window-top', }); const p = fraction.top; // 0 .. 1 return ( <div ref={ref}> {/* progress bar */} <div style={{ width: `${p * 100}%` }} /> {/* fade + rise in */} <div style={{ opacity: p, transform: `translateY(${(1 - p) * 40}px)`, }} /> {/* parallax layers */} <div style={{ transform: `translateY(${p * -80}px)` }} /> </div> ); }
Feed it a sequence of images and it draws one frame per scroll step to a canvas — preloading, scaling and cover-fitting for you. Scroll through the section below to flip all 162 frames.
Frames are painted to a single canvas, so playback stays smooth even with hundreds of images.
Loads frames just ahead of the scroll and supports avif / webp / jpg per encode.
Swap frame sets at breakpoints so mobile and desktop each get the right assets.
npm i react-scroll-flip-book
Keep scrolling — every frame is drawn to a canvas in sync with the scrollbar. No video, no timeline. Just images.
Everything you just scrolled through is driven by the snippet below. The height of the wrapper controls how much scrolling it takes to play; positionFixed pins the canvas while it plays.
import { ScrollFlipBook } from 'react-scroll-flip-book'; // Point it at a folder of numbered frames. const frames = Array.from({ length: 162 }, (_, i) => { const n = String(i + 1).padStart(3, '0'); return { avif: `/frames/${n}.avif`, jpg: `/frames/${n}.jpg` }; }); export function Hero() { return ( // The taller the wrapper, the slower the flip. <div style={{ height: '320vh' }}> <ScrollFlipBook defaultSource={{ framePaths: frames }} animationStartPosition="window-bottom" animationEndPosition="window-top" positionFixed /> </div> ); }