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