🎬 React Scroll Media
Production-ready, cinematic scroll sequences for React.

Zero scroll-jacking • Pure sticky positioning • 60fps performance
Installation • Usage • API • Examples
🌟 Overview
react-scroll-media is a lightweight library for creating Apple-style "scrollytelling" image sequences. It maps scroll progress to image frames deterministically, using standard CSS sticky positioning for a native, jank-free feel.
✨ Features
🚀 Native Performance
- Uses
requestAnimationFrame for buttery smooth 60fps rendering
- No Scroll Jacking — We never hijack the scrollbar. It works with native scrolling
- CSS Sticky — Uses relatively positioned containers with sticky inner content
🖼️ Flexible Loading
- Manual — Pass an array of image URLs
- Pattern — Generate sequences like
/img_{index}.jpg
- Manifest — Load sequences from a JSON manifest
🧠 Smart Memory Management
- Lazy Mode — Keeps only ±3 frames in memory for huge sequences (800+ frames)
- Eager Mode — Preloads everything for maximum smoothness on smaller sequences
- Decoding — Uses
img.decode() to prevent main-thread jank during painting
🛠️ Developer Experience
- Debug Overlay — Visualize progress and frame index in real-time
- Hooks — Exported
useScrollSequence for custom UI implementations
- TypeScript — First-class type definitions
- SSR Safe — Works perfectly with Next.js / Remix / Gatsby
- A11y — Built-in support for
prefers-reduced-motion and ARIA attributes
- Robust — Error boundaries and callbacks for image load failures
🤔 When to Use This vs Video?
| Quality | Compressed (artifacts) | ✨ Lossless / Exact Frames (CRISP) |
| Transparency | Difficult (needs webm/hevc) | ✨ Native PNG/WebP Transparency (Easy) |
| Scrubbing | Janky (keyframe dependency) | ✨ 1:1 Instant Scrubbing |
| Mobile | Auto-play often blocked | ✨ Works everywhere |
| File Size | ✨ Small | Large (requires optimization/lazy loading) |
💡 Use Scroll Sequence when you need perfect interaction, transparency, or crystal-clear product visuals (like Apple).
💡 Use Video for long, non-interactive backgrounds.
📦 Installation
npm install react-scroll-media
or
yarn add react-scroll-media
🚀 Usage
🎯 Basic Example
The simplest way to use it is with the ScrollSequence component.
import { ScrollSequence } from 'react-scroll-media';
const frames = [
'/images/frame_01.jpg',
'/images/frame_02.jpg',
];
export default function MyPage() {
return (
<div style={{ height: '200vh' }}>
<h1>Scroll Down</h1>
<ScrollSequence
source={{ type: 'manual', frames }}
scrollLength="300vh" // Determines how long the sequence plays
/>
<h1>Continue Scrolling</h1>
</div>
);
}
✨ Scrollytelling & Composition
You can nest components inside ScrollSequence. They will be placed in the sticky container and can react to the timeline.
📝 Animated Text (ScrollText)
Animate opacity and position based on scroll progress (0 to 1). Supports enter and exit phases.
import { ScrollSequence, ScrollText } from 'react-scroll-media';
<ScrollSequence source={...} scrollLength="400vh">
{/* Fade In (0.1-0.2) -> Hold -> Fade Out (0.8-0.9) */}
<ScrollText
start={0.1}
end={0.2}
exitStart={0.8}
exitEnd={0.9}
translateY={50}
className="my-text-overlay"
>
Cinematic Experience
</ScrollText>
</ScrollSequence>
💬 Word Reveal (ScrollWordReveal)
Reveals text word-by-word as you scroll.
import { ScrollWordReveal } from 'react-scroll-media';
<ScrollWordReveal
text="Experience the smooth cinematic scroll."
start={0.4}
end={0.6}
style={{ fontSize: '2rem', color: 'white' }}
/>
🔧 Advanced: Custom Hooks
For full control over the specialized UI, use the headless hooks.
useScrollSequence
Manages the canvas image controller.
import { useScrollSequence } from 'react-scroll-media';
const CustomScroller = () => {
const { containerRef, canvasRef, isLoaded } = useScrollSequence({ ... });
};
useScrollTimeline
Subscribe to the scroll timeline in any component.
import { useScrollTimeline } from 'react-scroll-media';
const MyComponent = () => {
const { subscribe } = useScrollTimeline();
useEffect(() => subscribe((progress) => {
console.log('Progress:', progress);
}), [subscribe]);
return <div>...</div>;
};
⚙️ Configuration
ScrollSequence Props
source | SequenceSource | Required | Defines where images come from. |
scrollLength | string | "300vh" | Height of the container (animation duration). |
memoryStrategy | "eager" | "lazy" | "eager" | Optimization strategy. |
lazyBuffer | number | 10 | Number of frames to keep loaded in lazy mode. |
fallback | ReactNode | null | Loading state component. |
accessibilityLabel | string | "Scroll sequence" | ARIA label for the canvas. Example: "360 degree view of the product". |
debug | boolean | false | Shows debug overlay. |
onError | (error: Error) => void | undefined | Callback fired when an image fails to load or initialization errors occur. |
📊 Performance & Compatibility
📦 Bundle Size
| Minified | ~22.0 kB |
| Gzipped | ~6.08 kB |
✨ Zero dependencies — Uses native Canvas API, no heavyweight libraries.
🌐 Browser Support
| Chrome | ✅ | Full support (OffscreenCanvas enabled) |
| Firefox | ✅ | Full support |
| Safari | ✅ | Full support (Desktop & Mobile) |
| Edge | ✅ | Full support |
| IE11 | ❌ | Not supported (Missing ES6/Canvas features) |
♿ Accessibility (A11y)
-
🎹 Keyboard Navigation — Users can scrub through the sequence using standard keyboard controls (Arrow Keys, Spacebar, Page Up/Down) because it relies on native scrolling.
-
🔊 Screen Readers — Add accessibilityLabel to ScrollSequence to provide a description for the canvas. Canvas has role="img".
-
🎭 Reduced Motion — Automatically detects prefers-reduced-motion: reduce. If enabled, ScrollSequence will disable the scroll animation and display the fallback content (if provided) or simply freeze the first frame to prevent motion sickness.
💾 Memory Usage (Benchmarks)
Tested on 1080p frames.
| 100 | eager | 30MB | Instant seeking, smooth. |
| 500 | eager | 46MB | High RAM usage. |
| 1000 | eager | 57MB | Very high RAM usage. |
| 100 | lazy | 25MB | Low memory usage. |
| 500 | lazy | 30MB | Low memory usage. |
| 1000 | lazy | 45MB | ⭐ Recommended. Kept flat constant. |
🛡️ Error Handling & Fallbacks
Network errors are handled gracefully. You can provide a fallback UI that displays while images are loading or if they fail.
<ScrollSequence
source={{ type: 'manifest', url: '/bad_url.json' }}
fallback={<div className="error">Failed to load sequence</div>}
onError={(e) => console.error("Sequence error:", e)}
/>
🚨 Error Boundaries
For robust production apps, wrap ScrollSequence in an Error Boundary to catch unexpected crashes:
class ErrorBoundary extends React.Component<
{ fallback: React.ReactNode, children: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<ScrollSequence source={...} />
</ErrorBoundary>
🔄 Multi-Instance & Nested Scroll
react-scroll-media automatically handles multiple instances on the same page. Each instance:
- Registers with a shared
RAF loop (singleton) for optimal performance.
- Calculates its own progress independently.
- Should have a unique
scrollLength or container setup.
🏗️ Architecture
📂 SequenceSource Options
1. Manual Mode (Pass array directly)
{
type: 'manual',
frames: ['/img/1.jpg', '/img/2.jpg']
}
2. Pattern Mode (Generate URLs)
{
type: 'pattern',
url: '/assets/sequence_{index}.jpg',
start: 1,
end: 100,
pad: 4
}
3. Manifest Mode (Fetch JSON)
{
type: 'manifest',
url: '/sequence.json'
}
💡 Note: Manifests are cached in memory by URL. To force a refresh, append a query param (e.g. ?v=2).
🎨 How it Works (The "Sticky" Technique)
Unlike libraries that use position: fixed or JS-based scroll locking (which breaks refreshing and feels unnatural), we use CSS Sticky Positioning.
🔧 Technical Breakdown
-
Container (relative) — This element has the height you specify (e.g., 300vh). It occupies space in the document flow.
-
Sticky Wrapper (sticky) — Inside the container, we place a div that is 100vh tall and sticky at top: 0.
-
Canvas — The <canvas> sits inside the sticky wrapper.
-
Math — As you scroll the container, the sticky wrapper stays pinned to the viewport. We calculate:
progress = -containerRect.top / (containerHeight - viewportHeight)
This gives a precise 0.0 to 1.0 value tied to the pixel position of the scrollbar.
💡 Memory Strategy
-
"eager" (Default) — Best for sequences < 200 frames. Preloads all images into HTMLImageElement instances. Instant seeking, smooth playback. High memory usage.
-
"lazy" — Best for long sequences (500+ frames). Only keeps the current frame and its neighbors in memory. Saves RAM, prevents crashes.
- Buffer size defaults to ±10 frames but can be customized via
lazyBuffer.
🐛 Debugging
Enable the debug overlay to inspect your sequence in production:
<ScrollSequence
source={...}
debug={true}
/>
Output:
Progress: 0.45
Frame: 45 / 100
This overlay is updated directly via DOM manipulation (bypassing React renders) for zero overhead.
📄 License
MIT © 2026 Thanniru Sai Teja
Made with ❤️ for the React community
⬆ Back to Top