react-scroll-media
v1.1.0
Published
Production-ready scroll-driven image sequence rendering component for React
Maintainers
Readme
🎬 React Scroll Media
Production-ready, cinematic scroll sequences for React.
Zero scroll-jacking • Pure sticky positioning • 60fps performance
🌐 Live Demo • 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
requestAnimationFramefor 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 ±10 frames (configurable) 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
useScrollSequencefor custom UI implementations - TypeScript — First-class type definitions
- SSR Safe — Works perfectly with Next.js / Remix / Gatsby
- A11y — Built-in support for
prefers-reduced-motionand ARIA attributes - Robust — Error boundaries and callbacks for image load failures
🤔 When to Use This vs Video?
💡 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-mediaor
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 = () => {
// ... setup refs
const { containerRef, canvasRef, isLoaded } = useScrollSequence({ ... });
// ... render custom structure
};useScrollTimeline
Subscribe to the scroll timeline in any component.
import { useScrollTimeline } from 'react-scroll-media';
const MyComponent = () => {
const { subscribe } = useScrollTimeline();
// Subscribe to progress (0-1)
useEffect(() => subscribe((progress) => {
console.log('Progress:', progress);
}), [subscribe]);
return <div>...</div>;
};🎨 Image Fit Modes
The fit prop controls how images scale within the viewport, using the standard CSS object-fit property.
| Mode | Description | Best Use Case |
|------|-------------|---------------|
| cover (Default) | Fills the screen, cropping edges if aspect ratios differ. | Full-screen background sequences. |
| contain | Shows the full image. Letterboxing (bars) may appear. | Product showcases where no part of the image should be cut off. |
| fill | Stretches to fill dimensions. Ignores aspect ratio. | Abstract patterns where distortion is acceptable. |
| none | Original size. No scaling. | Pixel-perfect displays when the wrapper matches image size. |
| scale-down | Smallest of none or contain. | Responsive layouts where images shouldn't upscale beyond native resolution. |
⚙️ Configuration
ScrollSequence Props
📊 Performance & Compatibility
📦 Bundle Size
| Metric | Size | |--------|------| | Minified | ~23.72 kB | | Gzipped | ~7.11 kB |
✨ Zero dependencies. Uses native Canvas API, no heavyweight libraries.
🌐 Browser Support
♿ 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
accessibilityLabeltoScrollSequenceto provide a description for the canvas. Canvas hasrole="img".🎭 Reduced Motion — Automatically detects
prefers-reduced-motion: reduce. If enabled,ScrollSequencewill disable the scroll animation and display thefallbackcontent (if provided) or simply freeze the first frame to prevent motion sickness.
💾 Memory Usage (Benchmarks)
Tested on 1080p frames.
🛡️ 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;
}
}
// Usage
<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
RAFloop (singleton) for optimal performance. - Calculates its own progress independently.
- Should have a unique
scrollLengthor 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', // {index} is replaced
start: 1, // Start index
end: 100, // End index
pad: 4 // Zero padding (e.g. 1 -> 0001)
}3. Manifest Mode (Fetch JSON)
{
type: 'manifest',
url: '/sequence.json'
}
// JSON format: { "frames": ["url1", "url2"] } OR pattern config💡 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 adivthat is100vhtall andstickyattop: 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. This value is then mapped to the corresponding frame index:
frameIndex = Math.floor(progress * (totalFrames - 1))This approach ensures:
- Zero Jitter: The canvas position is handled by the browser's compositor thread (CSS Sticky).
- Native Feel: Momentum scrolling works perfectly on touchpads and mobile.
- Exact Sync: The frame updates are synchronized with the scroll position in a
requestAnimationFrameloop.
💡 Memory Strategy
"eager" (Default) — Best for sequences < 200 frames. Preloads all images into
HTMLImageElementinstances. 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.
- Buffer size defaults to ±10 frames but can be customized via
🐛 Debugging
Enable the debug overlay to inspect your sequence in production:
<ScrollSequence
source={...}
debug={true}
/>Output:
Progress: 0.45
Frame: 45 / 100This overlay is updated directly via DOM manipulation (bypassing React renders) for zero overhead.
🔒 Security
react-scroll-media prioritizes security and follows best practices for client-side rendering libraries.
Network Access
When using Manifest Mode (source.type === 'manifest'), the library makes network requests to fetch your manifest configuration. For security recommendations and best practices, see our SECURITY.md document.
Key Points:
- ✅ Network access is optional (only when using manifest mode)
- ✅ No external dependencies or third-party integrations
- ✅ All processing happens client-side
- ✅ No data collection or telemetry
Built-in Security Hardening (v1.0.5+):
- 🔐 HTTPS enforcement — HTTP manifest URLs are rejected
- 🔐 Credential isolation —
credentials: 'omit'prevents cookie/auth leakage - 🔐 Referrer protection —
referrerPolicy: 'no-referrer'prevents page URL leakage - 🔐 Response size limit — 1MB max to prevent memory exhaustion
- 🔐 Frame URL whitelist — Only
http:/https:and relative paths allowed;//evil.comrejected - 🔐 Frame count cap — Default 2000, ceiling 8000, configurable via
REACT_SCROLL_MEDIA_MAX_FRAMES - 🔐 Cache size limit — 50 entry cap with automatic eviction
- 🔐 Timeout protection — 10-second abort on slow responses
- 🔐 Response validation — Content-type + structure checks
Quick Security Tips:
- Use HTTPS for all manifest URLs
- Only load manifests from trusted sources
- Use Manual or Pattern modes for sensitive environments
- Implement Content Security Policy headers
📄 License
MIT © 2026 Thanniru Sai Teja
Made with ❤️ for the React community
