Skip to Content
V2 (beta)React & Next.js

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.tgz

Automatic 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.

📄SceneMinimal.tsx
Open preview↗
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>
  1. <Scene>: The root controller. It creates the sticky scroll container and determines how many pixels (duration) the user must scroll before the scene unpins.
  2. <Layer>: Positioned absolutely within the Scene container. Anchors content or canvas elements according to a 9-point grid (placement="center center", "top left", etc.).
  3. <Canvas2d>: Renders frames onto an HTML5 <canvas> using high-speed CanvasRenderingContext2D or WebGLRenderingContext (when depth maps are present).

4. Complete Props Reference

<Scene> Props

<Scene> accepts all properties defined by SceneAttributes plus standard React container props:

PropTypeDefaultDescription
refRef<SceneHandle>undefinedImperative handle for controlling progress via code without React re-renders.
durationnumber1500Pin duration in pixels that the user must scroll to complete the scene.
durationUnit'px''px'Unit of pin duration. Only 'px' is supported.
heightnumber100Scene viewport height value.
heightUnit'vh' | 'px' | '%' | 'em' | 'rem' | 'vw''vh'CSS unit for scene viewport height.
topnumber0Viewport offset top when pinned in pixels.
topUnit'px''px'Unit for viewport top offset. Only 'px' is supported.
triggerStartnumber0.5Viewport trigger start point (0 = top of viewport, 0.5 = center, 1 = bottom).
triggerEndnumber0.5Viewport trigger end point where the sequence unpins.
scrubnumber1Smoothing 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.
contentAnimationsContentAnimationsundefinedDeclarative timeline animations bound to the global scene progress (0 to 1).
markersbooleanfalseVisual debug markers for start/end triggers during development.
classNamestring''Custom CSS class name applied to .scrollsequence-scene.
childrenReactNodeundefinedChild 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.

PropTypeDefaultDescription
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.
classNamestring''Custom CSS class names applied to .scrollsequence-layer.
childrenReactNodeundefinedElements 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:

PropTypeDefaultDescription
refRef<Canvas2dHandle>undefinedImperative handle to scrub or inspect the canvas block directly.
mediaGroupsMediaGroup[][]Array of image sequence sources (manifests, URL lists, or gaps).
widthnumber100Canvas width value.
widthUnitstring'%'Unit for canvas width ('%', 'px', 'vw').
heightnumber100Canvas height value.
heightUnitstring'vh'Unit for canvas height ('vh', 'px', '%').
disableDepthmapsbooleanfalseForce-disables WebGL 3D depth shaders even when grayscale depth maps are present.
depthMapsTiltnumber1.5Intensity multiplier for 3D mouse parallax tilt.
depthMapsZoomnumber1.05Boundary scale zoom factor to hide canvas edges during 3D parallax tilt.
scrubnumber1Smoothing scrub factor in seconds when used standalone.
triggerStartnumber0.5Viewport trigger start point (0 = top, 0.5 = center, 1 = bottom).
triggerEndnumber0.5Viewport trigger end point.
markersbooleanfalseVisual debug markers for start/end triggers in development.
classNamestring''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:

📄CanvasMinimal.tsx
Open preview↗
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:

📄SceneDepth.tsx
Open preview↗
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:

MethodSignatureDescription
progress()() => number | undefinedGetter: Returns current sequence progress (0.0 to 1.0).
progress(val)(val: number) => numberSetter: Sets progress to val (0.0 to 1.0) and locks manual control.
progress(null)(val: null) => numberRelease: Releases manual control and returns sequence to scroll-driven playback.
getBlock()() => BlockScene | BlockC2dReturns the underlying runtime engine block instance.
getElement()() => HTMLDivElement | nullReturns the underlying DOM container element.

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> ); }
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:

📄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} 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.

📄SceneContent.tsx
Open preview↗
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 from 0 (scene enter) to 1 (scene exit). Ideal for persistent HUDs or global scroll progress bars.
  • MediaGroup-Level Timeline: Attached directly inside a mediaGroup.contentAnimations. Progress spans 0 to 1 across 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 (0 to 1).
  • autoAlpha: GSAP-style shorthand that combines opacity and CSS visibility (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):

ValueOpacityVisibilityPointer / Click State
autoAlpha: 00'hidden'Non-blocking (clicks pass through)
autoAlpha: > 0Animated 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';
Last updated on
Feedback