Skip to Content
Coming soon!
V2 (beta)React & Next.jsProgress & Mouse Control

Progress & Mouse Control

Scrollsequence gives you programmatic control over scene playback without causing React re-renders. By using the imperative SceneHandle ref, you can drive the sequence with custom UI sliders, pointer/mouse movements, or custom timeline engines at full 60–144 FPS.


1. Imperative Progress Control (SceneHandle)

Attach a useRef<SceneHandle>(null) to your <Scene>:

import { useRef } from 'react'; import { Scene, SceneHandle } from 'scrollsequence/react'; export default function MyScene() { const sceneRef = useRef<SceneHandle>(null); // Set scene progress programmatically (0.0 to 1.0) const setFrame = (progress: number) => { sceneRef.current?.progress(progress); }; // Reset back to scroll-driven playback const resetToScroll = () => { sceneRef.current?.progress(null); }; return <Scene ref={sceneRef}>{/* ... */}</Scene>; }

Slider Scrubbing Example

📄SceneProgressControl.tsx
Open preview↗
import { useState, useRef } from 'react'; import { Scene, Layer, Canvas2d, SceneHandle } from 'scrollsequence/react'; import manifestBlue from '../manifests/dna-blue-no-depth/scrollsequence-manifest.json'; import manifestRed from '../manifests/dna-red-no-depth/scrollsequence-manifest.json'; export default function SceneProgressControl() { // Imperative ref handle (high-performance 60+ FPS, zero React re-renders) const sceneRef = useRef<SceneHandle>(null); const [displayVal, setDisplayVal] = useState<string>('scroll'); return ( <main className="min-h-screen bg-neutral-950 text-white"> {/* Floating Interactive Test Panel for React */} <div className="fixed top-6 left-1/2 -translate-x-1/2 z-50 bg-neutral-900/95 backdrop-blur-md border border-neutral-700 rounded-2xl p-4 shadow-2xl flex flex-col gap-3 text-xs font-sans max-w-md w-[92vw] sm:w-[460px]"> <div className="flex items-center justify-between border-b border-neutral-800 pb-2"> <span className="font-semibold text-neutral-200">Scene Progress Control (via useRef)</span> <button onClick={() => { sceneRef.current?.progress(null); setDisplayVal('scroll'); }} className="bg-blue-600 hover:bg-blue-500 active:bg-blue-700 text-white px-3 py-1 rounded-md cursor-pointer font-medium transition-colors" > Reset to Scroll </button> </div> {/* Manual Scrub Slider via Ref */} <div className="flex items-center justify-between gap-3"> <span className="text-neutral-400 font-medium">Slider:</span> <input type="range" min="0" max="1" step="0.001" defaultValue="0" aria-label="Scene progress control slider" onChange={(e) => { const val = parseFloat(e.target.value); sceneRef.current?.progress(val); setDisplayVal(val.toFixed(3)); }} className="flex-1 accent-blue-500 cursor-pointer" /> <span className="font-mono text-neutral-300 w-12 text-right">{displayVal}</span> </div> </div> {/* Spacer before sequence */} <section className="h-[60vh] flex flex-col items-center justify-center text-center px-4 text-neutral-400"> <h2 className="text-xl font-semibold text-white">Scene Progress Control ↓</h2> <p className="mt-2 text-sm max-w-lg"> Try to control scene progress via slider. Can be mapped to any custom progress. </p> </section> {/* Pinned Scene with Content Animations */} <Scene ref={sceneRef} contentAnimations={{ onScroll: [ { selector: '.animation-attached-to-global-scene-progress', track: { 0: { width: 'calc(0% + 2px)' }, 1: { width: 'calc(100% - 2px)' }, }, }, ], }} > <Layer> <Canvas2d disableDepthmaps={true} mediaGroups={[ { type: 'manifest', rootDir: 'https://cdn.scrollsequence.com/2026-09/dna-blue/', imageSizes: manifestBlue.imageSizes, contentAnimations: { onScroll: [ { selector: '.animation-attached-to-first-media-group', track: { 0: { opacity: 0, y: '100px', scale: 0.8 }, 0.25: { opacity: 1, y: 0, scale: 1 }, 0.75: { opacity: 1, y: 0, scale: 1 }, 1: { opacity: 0, y: '-100px', scale: 0.8 }, }, }, ], }, }, { type: 'manifest', rootDir: 'https://cdn.scrollsequence.com/2026-09/dna-red/', imageSizes: manifestRed.imageSizes, }, ]} /> </Layer> {/* Global Progress Bar Layer */} <Layer placement="top left"> <div className="w-full"> <div className="animation-attached-to-global-scene-progress h-1 bg-red-500" /> </div> </Layer> {/* Dynamic Animated Text Layer */} <Layer placement="center center"> <div className="animation-attached-to-first-media-group text-center px-4"> <h1 className="text-4xl sm:text-6xl font-extrabold tracking-tight"> Animate Any HTML Content </h1> <p className="mt-3 text-lg sm:text-xl text-neutral-300"> Sync typography, UI layers, and progress indicators directly to scroll </p> </div> </Layer> </Scene> {/* Spacer after sequence */} <section className="h-screen flex items-center justify-center text-neutral-400"> <p className="text-lg">Sequence ended, continue reading...</p> </section> </main> ); }

2. Interactive Mouse / Pointer Control

Instead of binding to scroll, you can track the user’s cursor position or device orientation to drive the sequence and apply real-time 3D depth tilt:

📄MouseControl.tsx
Open preview↗
import { useEffect, useRef } from 'react'; import { Scene, Layer, Canvas2d, SceneHandle } from 'scrollsequence/react'; import manifest from '../manifests/handmove/scrollsequence-manifest.json'; export default function MouseControl() { const sceneRef = useRef<SceneHandle>(null); const progressTextRef = useRef<HTMLSpanElement>(null); const progressBarRef = useRef<HTMLDivElement>(null); const rafIdRef = useRef<number | null>(null); const latestProgressRef = useRef<number>(0); const hasMouse = window.matchMedia('(hover: hover) and (pointer: fine)').matches; useEffect(() => { const handlePointerMove = (e: MouseEvent | PointerEvent) => { if (!hasMouse) return; const height = window.innerHeight; if (height <= 0) return; // Mouse at top (clientY = 0) => progress = 0 // Mouse at bottom (clientY = height) => progress = 1 const progress = Math.min(1, Math.max(0, e.clientY / height)); latestProgressRef.current = progress; if (rafIdRef.current === null) { rafIdRef.current = requestAnimationFrame(() => { rafIdRef.current = null; const current = latestProgressRef.current; // Scrub scene frame imperatively sceneRef.current?.progress(current); // Update HUD directly for zero-latency 60-144 FPS if (progressTextRef.current) { progressTextRef.current.textContent = current.toFixed(3); } if (progressBarRef.current) { progressBarRef.current.style.width = `${(current * 100).toFixed(1)}%`; } }); } }; window.addEventListener('pointermove', handlePointerMove); return () => { window.removeEventListener('pointermove', handlePointerMove); if (rafIdRef.current !== null) { cancelAnimationFrame(rafIdRef.current); rafIdRef.current = null; } }; }, []); return ( <main> <Scene ref={sceneRef} > <Layer> <Canvas2d depthMapsTilt={0.28} mediaGroups={[{ type: 'manifest', rootDir: 'https://cdn.scrollsequence.com/2026-09/handmove/', imageSizes: manifest.imageSizes, contentAnimations: { onScroll: [{ selector: '.track-hand', track: { 0: { translateY: '0px' }, 1: { translateY: '100%' } } }], }, }]} /> </Layer> <Layer placement="center center"> <div className="track-hand"> {hasMouse ? <MouseCard progressTextRef={progressTextRef} progressBarRef={progressBarRef} /> : <TouchCard /> } </div> </Layer> </Scene> </main> ); } // --- Component A: Mouse Controller Card --- function MouseCard({ progressTextRef, progressBarRef }: { progressTextRef: any, progressBarRef: any }) { return ( <div className="w-[320px] sm:w-[350px] rounded-2xl bg-neutral-950/70 backdrop-blur border border-emerald-500/30 p-5 shadow-[0_0_35px_rgba(16,185,129,0.25)] text-white text-left font-sans"> <div className="flex items-center justify-between pb-3 border-b border-white/10 text-xs"> <span className="flex items-center gap-2"><span className="h-2 w-2 rounded-full bg-emerald-400 animate-pulse" /><strong className="tracking-wide uppercase text-[11px] opacity-80">Scrollsequence</strong></span> <span className="px-2 py-0.5 rounded-full bg-emerald-500/10 border border-emerald-500/30 text-[10px] text-emerald-300 font-mono">Mouse Active • 60 FPS</span> </div> <div className="py-3 space-y-1"> <h2 className="text-xl sm:text-2xl font-extrabold bg-linear-to-r from-white via-emerald-100 to-emerald-300 bg-clip-text text-transparent">Mouse Control</h2> <p className="text-xs text-neutral-300 leading-relaxed">Move mouse to scrub frames & tilt</p> </div> <div className="space-y-1.5 pt-1"> <div className="flex justify-between text-[11px] font-mono text-neutral-400"> <span>Controller: <strong className="text-emerald-300">Mouse</strong></span> <span>Progress: <strong className="text-white"><span ref={progressTextRef}></span></strong></span> </div> <div className="w-full h-1.5 bg-neutral-800/80 rounded-full overflow-hidden border border-neutral-700/60"> <div ref={progressBarRef} className="h-full bg-linear-to-r from-blue-500 via-indigo-500 to-emerald-400 transition-[width] duration-75 ease-out rounded-full" style={{ width: '0%' }} /> </div> </div> <div className="pt-3 mt-2 flex items-center justify-between text-[11px] text-neutral-400 border-t border-white/5"> <span className="flex items-center gap-1 text-neutral-300"> Depth tilt active </span> </div> </div> ); } // --- Component B: Touch-Scroll Controller Card --- function TouchCard() { return ( <div className="w-[320px] sm:w-[350px] rounded-2xl bg-neutral-950/70 backdrop-blur-2xl border border-amber-500/30 p-5 shadow-[0_0_35px_rgba(245,158,11,0.25)] text-white text-left font-sans"> <div className="flex items-center justify-between pb-3 border-b border-white/10 text-xs"> <span className="flex items-center gap-2"><span className="h-2 w-2 rounded-full bg-amber-400 animate-pulse" /><strong className="tracking-wide uppercase text-[11px] opacity-80">Warning</strong></span> <span className="px-2 py-0.5 rounded-full bg-amber-500/10 border border-amber-500/30 text-[10px] text-amber-300 font-mono">No Mouse</span> </div> <div className="py-3 space-y-1"> <h2 className="text-xl sm:text-2xl font-extrabold bg-linear-to-r from-white via-amber-100 to-amber-300 bg-clip-text text-transparent">Mouse Control</h2> <p className="text-xs text-neutral-300 leading-relaxed">No mouse detected. Scrub sequence by scrolling page</p> </div> </div> ); }
Last updated on
Feedback