React & Next.js Integration
Scrollsequence provides first-class React components via the scrollsequence/react export. It delivers hardware-accelerated 60+ FPS canvas scrubbing, automatic CSS style injection, WebGL 3D depth parallax, dynamic HTML content animations, and zero-re-render imperative progress controls.
1. Installation & Setup
Install the library in your React or Next.js project:
Public Beta Installation Notice: While our official NPM registry listing is pending trademark finalization, Scrollsequence is distributed directly as a pre-packaged archive (.tgz).
You can install directly from our official server via URL, or download scrollsequence-2.0.0-beta.52.tgz directly into your project. Once installed, all imports (scrollsequence/react) work as standard!
# Recommended: Install directly from URL
npm install scrollsequence@https://docs.scrollsequence.com/scrollsequence-2.0.0-beta.52.tgz
# Or with pnpm / yarn:
pnpm add scrollsequence@https://docs.scrollsequence.com/scrollsequence-2.0.0-beta.52.tgz
yarn add scrollsequence@https://docs.scrollsequence.com/scrollsequence-2.0.0-beta.52.tgz# Alternative: Local tarball (package.json "file:..." dependency)
# Download the .tgz to your project root, then:
npm install ./scrollsequence-2.0.0-beta.52.tgzAutomatic CSS Injection: Styles are automatically bundled and injected into <head> at runtime when importing from scrollsequence/react. No manual CSS import or link tag is required in your root layout!
Quick Import
import { Scene, Layer, Canvas2d } from 'scrollsequence/react';
import type { SceneHandle, Canvas2dHandle } from 'scrollsequence/react';2. Minimal Pinned Scene Showcase
In this setup, <Scene> pins the viewport while the user scrolls. The first <Layer> holds the background <Canvas2d> sequence, and the second <Layer> pins typography centered over the canvas.
import { Scene, Layer, Canvas2d } from 'scrollsequence/react';
import manifest from '../manifests/dna-blue-no-depth/scrollsequence-manifest.json'
export default function SceneMinimal() {
return (
<main className="min-h-screen bg-neutral-950 text-white">
{/* Spacer before sequence */}
<section className="h-[50vh] flex items-center justify-center text-neutral-400">
<p className="text-lg">Scroll down to start sequence ↓</p>
</section>
{/* Pinned Scene with canvas background & overlay text */}
<Scene>
<Layer>
<Canvas2d
mediaGroups={[
{
type: 'manifest',
rootDir: 'https://cdn.scrollsequence.com/2026-09/dna-blue/',
imageSizes: manifest.imageSizes,
},
]}
/>
</Layer>
<Layer placement="center center">
<div className="text-center px-4">
<h1 className="text-4xl sm:text-6xl font-extrabold tracking-tight">
Cinematic Scroll
</h1>
<p className="mt-3 text-lg sm:text-xl text-neutral-300">
60 FPS Hardware-Accelerated Scrubbing
</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>
);
}Basic Code Pattern
import { Scene, Layer, Canvas2d } from 'scrollsequence/react';
import manifest from '@/public/hero/scrollsequence-manifest.json';
export default function HeroScene() {
return (
<Scene duration={1500} triggerStart={0.5} triggerEnd={0.5}>
{/* Background Canvas Sequence Layer */}
<Layer>
<Canvas2d
mediaGroups={[
{
type: 'manifest',
rootDir: '/hero/',
imageSizes: manifest.imageSizes,
},
]}
/>
</Layer>
{/* Centered Overlay Content Layer */}
<Layer placement="center center">
<div className="text-center text-white">
<h1 className="text-5xl font-extrabold">Cinematic Scroll</h1>
<p className="mt-2 text-xl text-neutral-300">60 FPS Hardware-Accelerated Scrubbing</p>
</div>
</Layer>
</Scene>
);
}3. Architecture & Component Hierarchy
Scrollsequence organizes viewport scrubbing into three core components:
<Scene> <-- Pinned scroll container; coordinates scroll distance and timeline
<Layer> <-- 9-point spatial placement container inside pinned viewport
<Canvas2d /> <-- Hardware-accelerated 2D/WebGL canvas streaming image frames
</Layer>
<Layer> <-- Content overlay container (HTML typography, HUDs, buttons)
<div>...</div>
</Layer>
</Scene><Scene>: The root controller. It creates the sticky scroll container and determines how many pixels (duration) the user must scroll before the scene unpins.<Layer>: Positioned absolutely within the Scene container. Anchors content or canvas elements according to a 9-point grid (placement="center center","top left", etc.).<Canvas2d>: Renders frames onto an HTML5<canvas>using high-speedCanvasRenderingContext2DorWebGLRenderingContext(when depth maps are present).
4. Complete Props Reference
<Scene> Props
<Scene> accepts all properties defined by SceneAttributes plus standard React container props:
| Prop | Type | Default | Description |
|---|---|---|---|
ref | Ref<SceneHandle> | undefined | Imperative handle for controlling progress via code without React re-renders. |
duration | number | 1500 | Pin duration in pixels that the user must scroll to complete the scene. |
durationUnit | 'px' | 'px' | Unit of pin duration. Only 'px' is supported. |
height | number | 100 | Scene viewport height value. |
heightUnit | 'vh' | 'px' | '%' | 'em' | 'rem' | 'vw' | 'vh' | CSS unit for scene viewport height. |
top | number | 0 | Viewport offset top when pinned in pixels. |
topUnit | 'px' | 'px' | Unit for viewport top offset. Only 'px' is supported. |
triggerStart | number | 0.5 | Viewport trigger start point (0 = top of viewport, 0.5 = center, 1 = bottom). |
triggerEnd | number | 0.5 | Viewport trigger end point where the sequence unpins. |
scrub | number | 1 | Smoothing scrub factor in seconds (0 = instant snap, 1 = standard smoothing). |
scenePosition | 'sticky' | 'flow' | 'fixed' | 'sticky' | Container positioning behavior. |
stickyType | 'css' | 'js' | 'css' | Pinning engine: native CSS position: sticky (recommended) or JS transforms. |
contentAnimations | ContentAnimations | undefined | Declarative timeline animations bound to the global scene progress (0 to 1). |
markers | boolean | false | Visual debug markers for start/end triggers during development. |
className | string | '' | Custom CSS class name applied to .scrollsequence-scene. |
children | ReactNode | undefined | Child components (typically <Layer> elements). |
<Layer> Props
<Layer> is a spatial positioning container. It aligns HTML content or <Canvas2d> inside the Scene’s pinned inner box.
| Prop | Type | Default | Description |
|---|---|---|---|
placement | 'top left' | 'top center' | 'top right' | 'center left' | 'center center' | 'center right' | 'bottom left' | 'bottom center' | 'bottom right' | 'center center' | 9-point spatial viewport anchor for layer content. |
className | string | '' | Custom CSS class names applied to .scrollsequence-layer. |
children | ReactNode | undefined | Elements rendered inside the layer. |
Note on Styling <Layer>: <Layer> does not accept an inline style prop. To apply custom styles (backgrounds, paddings, filters), apply classes via className or style a child wrapper <div> directly inside the layer.
<Canvas2d> Props
<Canvas2d> accepts all properties defined by Canvas2dAttributes plus className and ref:
| Prop | Type | Default | Description |
|---|---|---|---|
ref | Ref<Canvas2dHandle> | undefined | Imperative handle to scrub or inspect the canvas block directly. |
mediaGroups | MediaGroup[] | [] | Array of image sequence sources (manifests, URL lists, or gaps). |
width | number | 100 | Canvas width value. |
widthUnit | string | '%' | Unit for canvas width ('%', 'px', 'vw'). |
height | number | 100 | Canvas height value. |
heightUnit | string | 'vh' | Unit for canvas height ('vh', 'px', '%'). |
disableDepthmaps | boolean | false | Force-disables WebGL 3D depth shaders even when grayscale depth maps are present. |
depthMapsTilt | number | 1.5 | Intensity multiplier for 3D mouse parallax tilt. |
depthMapsZoom | number | 1.05 | Boundary scale zoom factor to hide canvas edges during 3D parallax tilt. |
scrub | number | 1 | Smoothing scrub factor in seconds when used standalone. |
triggerStart | number | 0.5 | Viewport trigger start point (0 = top, 0.5 = center, 1 = bottom). |
triggerEnd | number | 0.5 | Viewport trigger end point. |
markers | boolean | false | Visual debug markers for start/end triggers in development. |
className | string | '' | Custom CSS class names applied to .scrollsequence-canvas2d. |
5. Standalone Unpinned Canvas
<Canvas2d> can be used outside of <Scene> as an unpinned inline element. In this mode, the canvas scrubs frames automatically relative to its own visibility in the page flow:
import { Canvas2d } from 'scrollsequence/react';
import manifest from '../manifests/dna-blue-no-depth/scrollsequence-manifest.json'
export default function CanvasMinimal() {
return (
<main className="min-h-screen bg-neutral-950 text-white">
{/* Spacer before sequence */}
<section className="h-[50vh] flex items-center justify-center text-neutral-400">
<p className="text-lg">Scroll down to start sequence ↓</p>
</section>
{/* Scroll-driven canvas scrubbing */}
<Canvas2d
mediaGroups={[
{
type: 'manifest',
rootDir: 'https://cdn.scrollsequence.com/2026-09/dna-blue/',
imageSizes: manifest.imageSizes,
},
]}
/>
{/* 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>
);
}import { Canvas2d } from 'scrollsequence/react';
import manifest from '@/public/hero/scrollsequence-manifest.json';
export default function StandaloneCanvas() {
return (
<section className="py-20 flex justify-center">
<Canvas2d
width={80}
widthUnit="%"
height={60}
heightUnit="vh"
scrub={0.5}
mediaGroups={[
{
type: 'manifest',
rootDir: '/hero/',
imageSizes: manifest.imageSizes,
},
]}
/>
</section>
);
}6. 3D WebGL Depth Parallax
When your manifest contains grayscale depth maps (files.d), <Canvas2d> automatically detects them and initializes an internal WebGL shader. Moving your cursor or tilting a mobile device displaces the pixels in 2.5D space:
import { Scene, Layer, Canvas2d } from 'scrollsequence/react';
import manifest from '../manifests/dna-blue/scrollsequence-manifest.json'
export default function SceneDepth() {
return (
<main className="min-h-screen bg-neutral-950 text-white">
{/* Spacer before sequence */}
<section className="h-[50vh] flex items-center justify-center text-neutral-400">
<p className="text-lg">Scroll down to start sequence ↓</p>
</section>
{/* Pinned Scene with Depth Map Parallax */}
<Scene>
<Layer>
<Canvas2d
mediaGroups={[
{
type: 'manifest',
rootDir: 'https://cdn.scrollsequence.com/2026-09/dna-blue/',
imageSizes: manifest.imageSizes,
},
]}
/>
</Layer>
<Layer placement="center center">
<div className="text-center px-4">
<h1 className="text-4xl sm:text-6xl font-extrabold tracking-tight">
Cinematic Depth & Parallax
</h1>
<p className="mt-3 text-lg sm:text-xl text-neutral-300">
Hardware-accelerated 2.5D depth map simulation
</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>
);
}<Canvas2d
depthMapsTilt={1.8} // Higher = more dramatic 3D displacement
depthMapsZoom={1.08} // Prevents clipping around outer canvas borders
disableDepthmaps={false} // Set to true to bypass WebGL and render 2D canvas
mediaGroups={[
{
type: 'manifest',
rootDir: 'https://cdn.scrollsequence.com/2026-09/dna-blue/',
imageSizes: manifest.imageSizes,
},
]}
/>7. Imperative Progress & Hardware Control (SceneHandle & Canvas2dHandle)
Scrollsequence provides imperative ref handles that let you control sequence playback programmatically (e.g. from UI sliders, pointer tracking, gyroscopes, or custom animation engines) at 60–144 FPS with zero React re-renders.
Handle Methods Reference
Both SceneHandle and Canvas2dHandle expose the following methods:
| Method | Signature | Description |
|---|---|---|
progress() | () => number | undefined | Getter: Returns current sequence progress (0.0 to 1.0). |
progress(val) | (val: number) => number | Setter: Sets progress to val (0.0 to 1.0) and locks manual control. |
progress(null) | (val: null) => number | Release: Releases manual control and returns sequence to scroll-driven playback. |
getBlock() | () => BlockScene | BlockC2d | Returns the underlying runtime engine block instance. |
getElement() | () => HTMLDivElement | null | Returns the underlying DOM container element. |
Slider Scrubbing Example
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>
);
}import { useRef } from 'react';
import { Scene, Layer, Canvas2d, SceneHandle } from 'scrollsequence/react';
export default function ManualScrubScene() {
const sceneRef = useRef<SceneHandle>(null);
return (
<main>
{/* UI Control Bar */}
<div className="fixed top-4 left-4 z-50 flex gap-2 bg-neutral-900/90 p-3 rounded-lg text-white">
<input
type="range"
min="0"
max="1"
step="0.001"
onChange={(e) => sceneRef.current?.progress(parseFloat(e.target.value))}
/>
<button onClick={() => sceneRef.current?.progress(null)}>
Reset to Scroll
</button>
</div>
<Scene ref={sceneRef}>
<Layer>
<Canvas2d mediaGroups={[/* ... */]} />
</Layer>
</Scene>
</main>
);
}Pointer & Mouse Control
You can track mouse movements to scrub frames and tilt depth maps simultaneously:
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}
scenePosition={hasMouse ? "flow" : "sticky" } // use to reduce container height
>
<Layer>
<Canvas2d
depthMapsTilt={0.28}
mediaGroups={[{
type: 'manifest',
rootDir: 'https://cdn.scrollsequence.com/2026-09/handmove/',
imageSizes: manifest.imageSizes,
contentAnimations: {
onScroll: [{ selector: '.track-hand-and-animate', track: { 0: { translateY: '0px' }, 1: { translateY: '100%' } } }],
},
}]}
/>
</Layer>
<Layer placement="center center">
<div className="track-hand-and-animate">
{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-purple-500/30 p-5 shadow-[0_0_35px_rgba(168,85,247,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">Touch Control</h2>
<p className="text-xs text-neutral-300 leading-relaxed">No mouse detected. Scrub sequence by scrolling page</p>
</div>
</div>
);
}useEffect(() => {
const onPointerMove = (e: PointerEvent) => {
const progress = Math.min(1, Math.max(0, e.clientY / window.innerHeight));
sceneRef.current?.progress(progress);
};
window.addEventListener('pointermove', onPointerMove);
return () => window.removeEventListener('pointermove', onPointerMove);
}, []);8. Media Groups & Autoplay Specification
A <Canvas2d> sequence is composed of one or more Media Groups. Media groups play sequentially based on their scroll weights or can be configured to play autonomously.
Supported MediaGroup Types
export type MediaGroup = ImageGroupManifest | ImageGroupUrlList | ImageGroupGap;1. Manifest Sequence (ImageGroupManifest)
Powered by a scrollsequence-manifest.json file generated via the Scrollsequence CLI or Media Tools:
{
type: 'manifest',
rootDir: '/hero/', // Base directory or CDN URL
imageSizes: manifest.imageSizes, // Multi-resolution variants
tracking?: manifest.tracking, // AI subject bounding coordinates
isReverse?: false, // Plays from end to start frame
start?: 0, // Frame start index
end?: 240, // Frame end index
step?: 1, // Skip frames (e.g. 2 for every second frame)
scrollWeight?: 1, // Relative scroll length weight
bgSize?: 'cover' | 'contain', // Viewport scaling mode
bgPosition?: { x: 0.5, y: 0.5 }, // Anchor position center
autoplay?: false, // Autonomous playback config
contentAnimations?: { onScroll: [...] } // Animations scoped to this media group
}2. Raw URL List (ImageGroupUrlList)
Loads a sequence from an array of image URLs:
{
type: 'urlList',
images: ['0001.jpg', '0002.jpg', '0003.jpg'],
prefix: 'https://cdn.example.com/frames/',
suffix: '?v=2',
scrollWeight: 1,
}3. Spacer Gap (ImageGroupGap)
Introduces a pause in canvas scrubbing while user scroll continues (ideal for holding a static frame while text animates over it):
{
type: 'gap',
scrollWeight: 0.5, // 50% relative scroll length pause
}Autonomous Group Autoplay (autoplay)
By default, canvas frames scrub 1:1 with user scroll. You can enable autonomous playback so frames play continuously like a video clip when the user scrolls into view:
<Canvas2d
mediaGroups={[
{
type: 'manifest',
rootDir: '/intro/',
imageSizes: manifest.imageSizes,
autoplay: {
fps: 30, // Target playback frame rate (default: 30)
repeat: 0, // Number of repeats (0 = once, -1 = infinite loop)
repeatType: 'loop', // 'loop' | 'reverse' | 'mirror'
onEnter: 'play', // Action when scrolling into group: 'play' | 'reverse' | 'none'
onLeave: 'none', // Action when scrolling past group: 'none' | 'complete' | 'reset'
onEnterBack: 'reverse', // Action when scrolling up back into group
onLeaveBack: 'reset', // Action when scrolling up past group top
},
},
]}
/>9. Content Animations Timeline System
Scrollsequence allows you to synchronize any HTML typography, UI cards, or progress indicators directly to playback progress.
import { Scene, Layer, Canvas2d } 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 SceneContent() {
return (
<main className="min-h-screen bg-neutral-950 text-white">
{/* Spacer before sequence */}
<section className="h-[50vh] flex flex-col items-center justify-center text-center px-4 text-neutral-400">
<h2 className="text-xl font-semibold text-white">Scroll down to start sequence ↓</h2>
<p className="mt-2 text-sm max-w-lg">
We have two media groups. The <i>progress bar</i> is attached to the global Scene timeline,
and the <i>content animation</i> is attached to the media group.
</p>
</section>
{/* Pinned Scene with Content Animations */}
<Scene
contentAnimations={{
onScroll: [
{
selector: '.animation-attached-to-global-scene-progress',
track: {
0: { width: 'calc(0% + 2px)' },
1: { width: 'calc(100% - 2px)' },
},
},
],
}}
triggerStart={1}
triggerEnd={0}
>
<Layer>
<Canvas2d
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/',
isReverse: true, // second media group plays backwards
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>
);
}Scopes: Scene vs MediaGroup
- Scene-Level Timeline: Attached to
<Scene contentAnimations={{ onScroll: [...] }} />. Progress spans from0(scene enter) to1(scene exit). Ideal for persistent HUDs or global scroll progress bars. - MediaGroup-Level Timeline: Attached directly inside a
mediaGroup.contentAnimations. Progress spans0to1across that group’s specific frames. If groups are reversed or reordered, attached animations follow automatically.
Event Triggers (ContentAnimations)
Content animations support four distinct trigger modes:
export type ContentAnimations = {
onScroll?: ContentAnimationOnScroll[]; // Scrubbed continuously with scroll or threshold-triggered
onLoad?: ContentAnimation[]; // Triggered once when component mounts
onHover?: ContentAnimation[]; // Triggered when user hovers target element
onClick?: ContentAnimation[]; // Triggered on mouse click
};Keyframe Track Syntax
Each track definition maps milestones between 0 (or "0%") and 1 (or "100%") to CSS properties:
contentAnimations={{
onScroll: [
{
selector: '.animated-heading',
track: {
0: { autoAlpha: 0, y: '80px', scale: 0.85 },
0.25: { autoAlpha: 1, y: '0px', scale: 1 },
0.75: { autoAlpha: 1, y: '0px', scale: 1 },
1: { autoAlpha: 0, y: '-80px', scale: 0.85 },
},
progressSource: 'parent', // 'parent' (follows canvas frame progression) | 'scroll' (follows physical scroll)
autoplay: false, // Set to true or toggleActions config for discrete entrance triggers
options: {
ease: 'easeInOut',
duration: 0.8,
},
},
],
}}Supported Animated Properties
Scrollsequence is powered by Motion under the hood. You can animate any standard CSS property or transform that Motion supports, in addition to the specialized autoAlpha macro:
- Transforms:
x,y,z,scale,scaleX,scaleY,rotate,rotateX,rotateY,rotateZ,skew,skewX,skewY(numeric values or strings with CSS units:'100px','-50%','2rem','45deg'). - Layout & Sizing:
width,height,top,left,right,bottom,margin,padding,calc(...)expressions. - Visuals & Styling:
color,backgroundColor,borderColor,borderRadius,filter('blur(10px)','brightness(1.5)'),backdropFilter,clipPath,boxShadow. - Opacity: Standard CSS
opacity(0to1). autoAlpha: GSAP-style shorthand that combinesopacityand CSSvisibility(recommended for overlay content).
Understanding autoAlpha
When fading HTML overlays in or out during scroll sequences, using opacity: 0 alone leaves transparent DOM elements interactive — they continue to capture pointer events (mouse clicks, hovers, touches) and obstruct underlying elements.
Scrollsequence provides built-in support for autoAlpha (identical to GSAP’s autoAlpha):
| Value | Opacity | Visibility | Pointer / Click State |
|---|---|---|---|
autoAlpha: 0 | 0 | 'hidden' | Non-blocking (clicks pass through) |
autoAlpha: > 0 | Animated float (0 to 1) | 'visible' | Interactive |
// Example track using autoAlpha with transforms and filters
track: {
0: { autoAlpha: 0, y: '50px', filter: 'blur(10px)' },
0.3: { autoAlpha: 1, y: '0px', filter: 'blur(0px)' },
0.7: { autoAlpha: 1, y: '0px', filter: 'blur(0px)' },
1: { autoAlpha: 0, y: '-50px', filter: 'blur(10px)' },
}Why use autoAlpha instead of opacity?
autoAlpha automatically manages visibility: hidden whenever opacity drops to 0. This guarantees that hidden text layers, cards, or buttons never block user interaction with other elements or links underneath them during scroll scrubbing.
10. Framework Recipes & SSR Patterns
Next.js (App Router)
Because Scrollsequence interacts directly with browser APIs (window, HTMLCanvasElement, WebGLRenderingContext), client wrapper components must include 'use client':
// app/components/HeroSequence.tsx
'use client';
import { Scene, Layer, Canvas2d } from 'scrollsequence/react';
import manifest from '@/public/hero/scrollsequence-manifest.json';
export default function HeroSequence() {
return (
<Scene duration={1800}>
<Layer>
<Canvas2d
mediaGroups={[
{
type: 'manifest',
rootDir: '/hero/',
imageSizes: manifest.imageSizes,
},
]}
/>
</Layer>
<Layer placement="center center">
<h1 className="text-white text-6xl font-bold">Next.js App Router</h1>
</Layer>
</Scene>
);
}Public Directory Serving: When assets reside in ./public/hero/, setting rootDir: '/hero/' ensures the client browser fetches images directly from the web root.
Vite + React
Import components and assets directly with standard ESM imports:
import { Scene, Layer, Canvas2d } from 'scrollsequence/react';Astro
When using Astro with the @astrojs/react integration, apply the client:load or client:visible directive:
---
import HeroSequence from '../components/HeroSequence.tsx';
---
<HeroSequence client:load />11. TypeScript Re-exports & Constants
The scrollsequence/react entry point exports all components, handles, types, and defaults:
// Components
import { Scene, Layer, Canvas2d } from 'scrollsequence/react';
// Props & Handles
import type {
SceneProps,
SceneHandle,
LayerProps,
Canvas2dProps,
Canvas2dHandle,
} from 'scrollsequence/react';
// Default Attribute Objects
import { SCENE_DEFAULTS, LAYER_DEFAULTS } from 'scrollsequence/react';
// Media & Animation Types
import type {
SceneAttributes,
LayerAttributes,
Canvas2dAttributes,
MediaGroup,
MediaGroups,
ImageGroupManifest,
ImageGroupUrlList,
ImageGroupGap,
AutoplayC2dConfig,
ContentAnimations,
ContentAnimation,
ContentAnimationOnScroll,
ProgressSource,
ToggleActionsContentAnimation,
} from 'scrollsequence/react';