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

@primeinc/storyscroller

v0.9.0-beta.1

Published

Production-ready React component for narrative-driven scroll experiences with GSAP and Lenis

Readme

StoryScroller

npm version License: MIT TypeScript Bundle Size

A high-performance React component for narrative-driven scroll experiences. Built with GSAP and Lenis for buttery-smooth section-based navigation with full motion control.

Why StoryScroller?

  • 🎬 Narrative-First: Built specifically for storytelling experiences with chapter/scene support
  • ⚡ Blazing Fast: Hardware-accelerated animations via GSAP
  • 🎯 Precise Control: Frame-perfect scroll physics with customizable easing
  • 🔧 Production-Ready: Battle-tested with comprehensive error recovery
  • 📱 Universal: Works flawlessly on desktop, mobile, and everything in between

Installation

pnpm add @primeinc/storyscroller

Quick Start

import { StoryScroller } from '@primeinc/storyscroller'
import '@primeinc/storyscroller/styles'

function App() {
  const sections = [
    <section key="intro">
      <h1>Chapter 1</h1>
      <p>Your story begins...</p>
    </section>,
    <section key="conflict">
      <h1>Chapter 2</h1>
      <p>The plot thickens...</p>
    </section>,
    <section key="resolution">
      <h1>Chapter 3</h1>
      <p>And they lived happily ever after.</p>
    </section>
  ]

  return (
    <StoryScroller 
      sections={sections}
      onSectionChange={(index) => console.log(`Section ${index}`)}
    />
  )
}

Core Concepts

Sections

Each section is a full-viewport container that the scroller will snap to. Sections can contain any React content - from simple text to complex 3D scenes.

Physics-Based Scrolling

StoryScroller uses Lenis for momentum-based scrolling with configurable lerp values, creating natural-feeling navigation that responds to user intent.

Animation Queue

Navigation requests are intelligently queued and deduplicated, ensuring smooth transitions even with rapid user input.

Configuration

<StoryScroller
  sections={sections}
  
  // Animation
  duration={0.6}                    // Scroll animation duration (seconds)
  easing={(t) => t * t * t}        // Custom easing function
  
  // Physics
  tolerance={50}                    // Input sensitivity (higher = less sensitive)
  preventDefault={true}             // Prevent native scroll
  invertDirection={false}           // Invert scroll direction
  
  // Features
  keyboardNavigation={true}         // Arrow key support
  enableMagneticSnap={true}        // Magnetic section snapping
  magneticThreshold={0.15}         // Distance to trigger magnetic snap
  magneticVelocityThreshold={5}    // Max velocity for magnetic snap
  
  // Callbacks
  onSectionChange={(index) => {}}  // Section change handler
  
  // Accessibility
  ariaLabel="Story sections"       // Container ARIA label
  sectionLabels={["Intro", "Features", "Conclusion"]} // Custom section labels
  
  // Styling
  containerClassName="my-scroller"
  sectionClassName="my-section"
/>

Advanced Usage

Programmatic Control

function NavigationExample() {
  useEffect(() => {
    // Access the global API
    const api = window.storyScrollerAPI
    
    // Navigate programmatically
    api.gotoSection(2)      // Jump to section 3 (0-indexed)
    api.nextSection()       // Go forward
    api.prevSection()       // Go back
    
    // Get current state
    const state = api.getState()
    console.log(state.currentSection)
    console.log(state.isAnimating)
    
    // Emergency controls
    api.forceSync()         // Force sync scroll position
    api.emergencyReset()    // Nuclear reset
  }, [])

  return <StoryScroller sections={sections} />
}

Custom Navigation UI

function CustomNav() {
  const [state, setState] = useState({ 
    current: 0, 
    total: 5,
    isAnimating: false 
  })

  useEffect(() => {
    const interval = setInterval(() => {
      if (window.storyScrollerAPI?.getState) {
        const apiState = window.storyScrollerAPI.getState()
        setState({
          current: apiState.currentSection,
          total: 5,
          isAnimating: apiState.isAnimating
        })
      }
    }, 100)
    
    return () => clearInterval(interval)
  }, [])

  return (
    <nav className="fixed top-4 right-4 z-50">
      <button 
        onClick={() => window.storyScrollerAPI?.prevSection()}
        disabled={state.current === 0 || state.isAnimating}
      >
        Previous
      </button>
      
      <span>{state.current + 1} / {state.total}</span>
      
      <button 
        onClick={() => window.storyScrollerAPI?.nextSection()}
        disabled={state.current === state.total - 1 || state.isAnimating}
      >
        Next
      </button>
    </nav>
  )
}

With GSAP Animations

import { useGSAP } from '@gsap/react'
import gsap from 'gsap'

function AnimatedSection({ isActive, children }) {
  const containerRef = useRef()
  
  useGSAP(() => {
    if (isActive) {
      gsap.from(containerRef.current.children, {
        y: 100,
        opacity: 0,
        duration: 1,
        stagger: 0.1,
        ease: "power3.out"
      })
    }
  }, [isActive])
  
  return <div ref={containerRef}>{children}</div>
}

API Reference

Component Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | sections | ReactNode[] | required | Array of section components | | duration | number | 0.6 | Animation duration in seconds | | easing | (t: number) => number | Exponential ease-out | Easing function | | tolerance | number | 50 | Input sensitivity threshold | | preventDefault | boolean | true | Prevent native scroll | | invertDirection | boolean | false | Invert scroll direction | | keyboardNavigation | boolean | true | Enable keyboard controls | | enableMagneticSnap | boolean | true | Enable magnetic snapping | | magneticThreshold | number | 0.15 | Magnetic snap distance | | magneticVelocityThreshold | number | 5 | Max velocity for snap | | onSectionChange | (index: number) => void | - | Section change callback | | ariaLabel | string | "Story sections" | Container ARIA label | | sectionLabels | string[] | - | Custom labels for screen readers | | containerClassName | string | - | Container CSS class | | sectionClassName | string | - | Section CSS class |

Global API Methods

Available via window.storyScrollerAPI:

| Method | Returns | Description | |--------|---------|-------------| | gotoSection(index) | void | Navigate to specific section | | nextSection() | void | Go to next section | | prevSection() | void | Go to previous section | | getState() | ScrollState | Get current scroll state | | getQueueStatus() | QueueStatus | Get animation queue status | | forceSync() | void | Force sync to current position | | emergencyReset() | void | Reset entire system |

State Shape

interface ScrollState {
  // Position
  currentSection: number        // Current visible section
  targetSection: number | null  // Animation target
  scrollPosition: number        // Scroll position in pixels
  velocity: number              // Current scroll velocity
  
  // Status
  isAnimating: boolean          // Animation in progress
  isScrolling: boolean          // User scrolling
  canNavigate: boolean          // Ready for navigation
  
  // Timing
  lastNavigationTime: number    // Last navigation timestamp
  
  // Narrative (future features)
  narrativeMode: boolean        // Story mode active
  chapterIndex: number          // Current chapter
  sceneIndex: number            // Current scene
  transitionType: string        // Transition style
}

Performance Optimization

Bundle Size Analysis

Latest benchmark results (v1.0.0):

| Metric | Current | Target | Status | |--------|---------|--------|--------| | Core Bundle | 47.2KB | 50KB | ✅ Within target | | CSS Bundle | 0.1KB | - | ✅ Minimal | | Total Bundle | 47.3KB | 150KB | ✅ Excellent | | Gzipped | 14.2KB | 35KB | ✅ Optimal |

Performance Targets

Animation Performance

  • Navigation Duration: 600ms target
  • Input Response: < 100ms
  • Frame Rate: 60 FPS target, 45 FPS minimum

Memory Usage

  • Baseline: 10MB
  • Maximum: 50MB during heavy usage
  • Leak Tolerance: < 5MB

Real-World Benchmarks

  • ✅ Zero TypeScript errors
  • ✅ 3 lightweight runtime dependencies
  • ✅ 62.5% test coverage (improving to 80%+)
  • ✅ WCAG 2.1 AA accessibility compliance

Running Benchmarks

Generate fresh performance reports:

npm run benchmark          # Full performance analysis
npm run bundle:analyze     # Bundle size only
npm run test:all          # Performance + functionality tests

Optimization Tips

  1. Lazy Load Sections: Load section content on-demand
  2. Optimize Images: Use WebP/AVIF with proper sizing
  3. Debounce Callbacks: Throttle onSectionChange handlers
  4. CSS Containment: Use contain: layout style on sections
  5. Monitor Performance: Use npm run benchmark regularly

Accessibility

StoryScroller v1.0 is WCAG 2.1 AA compliant and built with accessibility as a priority:

  • 🎯 Keyboard Navigation: Full support for arrow keys, Page Up/Down, Home/End
  • 📢 Screen Reader Support: Live announcements with custom section labels
  • 🎨 High Contrast: Works with high contrast and reduced motion preferences
  • ⚡ Reduced Motion: Automatically disables animations when prefers-reduced-motion: reduce
  • 🔍 Focus Management: Proper focus indicators and tabindex management
  • 🆔 ARIA Labels: Comprehensive labeling for assistive technologies
  • 📱 CSS Fallback: Works without JavaScript using native scroll-snap

Accessibility Configuration

<StoryScroller
  sections={sections}
  ariaLabel="Story progression with 5 chapters"
  sectionLabels={[
    "Introduction to our product",
    "Key features overview", 
    "Customer testimonials",
    "Pricing information",
    "Get started today"
  ]}
/>

Browser Support

  • Chrome/Edge 90+
  • Firefox 88+
  • Safari 14+
  • Mobile Safari 14+
  • Chrome Android 90+

Troubleshooting

Mac Trackpad Sensitivity

Mac trackpads can be overly sensitive. Increase the tolerance prop:

<StoryScroller tolerance={80} />

Animation Jank

Ensure GPU acceleration:

.story-scroller-section {
  will-change: transform;
  transform: translateZ(0);
}

Memory Leaks

StoryScroller automatically cleans up, but ensure you're not creating closures in callbacks:

// ❌ Bad - creates new function every render
<StoryScroller onSectionChange={(i) => setSection(i)} />

// ✅ Good - stable reference
const handleChange = useCallback((i) => setSection(i), [])
<StoryScroller onSectionChange={handleChange} />

Examples

Marketing Site

<StoryScroller
  sections={[
    <HeroSection />,
    <FeaturesSection />,
    <TestimonialsSection />,
    <CTASection />
  ]}
  duration={0.8}
  enableMagneticSnap={true}
/>

Interactive Story

<StoryScroller
  sections={storyChapters}
  duration={1.5}
  easing={(t) => 1 - Math.pow(1 - t, 4)}
  onSectionChange={(chapter) => {
    playChapterMusic(chapter)
    updateProgressBar(chapter)
  }}
/>

Product Showcase

<StoryScroller
  sections={products.map(product => 
    <ProductSection key={product.id} {...product} />
  )}
  keyboardNavigation={true}
  tolerance={30}
/>

License

MIT © Prime Inc


Built with ❤️ using GSAP and Lenis