npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-scroll-media

v1.1.0

Published

Production-ready scroll-driven image sequence rendering component for React

Readme

🎬 React Scroll Media

Production-ready, cinematic scroll sequences for React.

npm version npm downloads package size security audit license

Zero scroll-jacking • Pure sticky positioning • 60fps performance

🌐 Live DemoInstallationUsageAPIExamples


🌟 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 ±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 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?

💡 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 = () => {
  // ... 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 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.

🛡️ 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:

  1. Registers with a shared RAF loop (singleton) for optimal performance.
  2. Calculates its own progress independently.
  3. 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', // {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

  1. Container (relative) — This element has the height you specify (e.g., 300vh). It occupies space in the document flow.

  2. Sticky Wrapper (sticky) — Inside the container, we place a div that is 100vh tall and sticky at top: 0.

  3. Canvas — The <canvas> sits inside the sticky wrapper.

  4. 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 requestAnimationFrame loop.

💡 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.


🔒 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 isolationcredentials: 'omit' prevents cookie/auth leakage
  • 🔐 Referrer protectionreferrerPolicy: '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.com rejected
  • 🔐 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

👉 Full Security Policy →


📄 License

MIT © 2026 Thanniru Sai Teja

Made with ❤️ for the React community

⬆ Back to Top