@primeinc/storyscroller
v0.9.0-beta.1
Published
Production-ready React component for narrative-driven scroll experiences with GSAP and Lenis
Maintainers
Readme
StoryScroller
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/storyscrollerQuick 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 testsOptimization Tips
- Lazy Load Sections: Load section content on-demand
- Optimize Images: Use WebP/AVIF with proper sizing
- Debounce Callbacks: Throttle
onSectionChangehandlers - CSS Containment: Use
contain: layout styleon sections - Monitor Performance: Use
npm run benchmarkregularly
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
