AI Website Guides · 2026-10-05 · 7 min read
Image Sequence Scroll Animation With Canvas: Build a Scroll-Driven Hero Step by Step
An image sequence scroll animation draws pre-rendered frames to a canvas as the visitor scrolls, so scrolling plays a film forward and back with no seek lag. This tutorial walks through the full build, from FFmpeg frame extraction to canvas drawing, Lenis smoothing, mobile and accessibility, with working code.
Written by AmJ, founder of Scrollhaus, and edited by the Scrollhaus editorial team. Sources and methodology are documented in the article.

Key takeaways
Disclosure: ScrollHaus publishes this tutorial. The technique below is the same canvas image sequence approach our own scroll-driven websites use, described from public browser APIs (canvas, requestAnimationFrame, FFmpeg) so you can reproduce it in any stack.
What is an image sequence scroll animation?
An image sequence animation on scroll, also called a scroll sequence, plays a pre-rendered series of frames as the visitor scrolls. Scroll position decides which frame is drawn, so scrolling forward plays the film forward and scrolling back rewinds it. Because every frame is a still image drawn to an HTML5 canvas, playback is exact and smooth in every browser. This is the technique behind the product pages that appear to scrub a film as you scroll.
Image sequence vs. video scrubbing
Many tutorials call the result a scroll sequence or a video scroll animation. The visitor sees a video that plays on scroll, but under the hood it is an image sequence animation drawn page by page, which is why it stays smooth.
You can set video.currentTime on scroll, but browsers decode video with keyframes, so seeking backward is slow and stutters. An image sequence has no seek cost: each frame is an independent image, decoded once and drawn instantly. The trade-off is weight, which the steps below manage with WebP and staged loading.
Step 1: Extract frames from your video with FFmpeg
Start from a clean source clip. Extract at a fixed frame rate (24 to 30 fps is plenty for scroll) and scale to the width you need. This command exports WebP frames at 1920 wide:
ffmpeg -i hero.mp4 -vf "fps=24,scale=1920:-2" -c:v libwebp -quality 82 frames/f_%04d.webp
A 6 second clip at 24 fps gives 144 frames. For mobile, export a second set at 960 wide and fewer frames (for example 12 fps) so phones download a fraction of the data.
Step 2: Preload frames in stages
Do not load every frame before showing anything. Load a coarse pass first (every 8th frame), draw the first frame immediately, then fill in the gaps. The animation is scrubbable in under a second and refines itself in the background.
const frames = new Array(TOTAL);
const load = (i) => new Promise((res) => {
const img = new Image();
img.decoding = 'async';
img.onload = () => { frames[i] = img; res(); };
img.src = `/frames/f_${String(i + 1).padStart(4, '0')}.webp`;
});
async function preload() {
for (const step of [8, 4, 2, 1]) {
const batch = [];
for (let i = 0; i < TOTAL; i += step) if (!frames[i]) batch.push(load(i));
await Promise.all(batch);
}
}
Step 3: Set up the canvas for sharp, high-DPI rendering
Size the canvas backing store by devicePixelRatio so it stays crisp on retina screens, and use a cover-fit calculation so the frame fills the viewport without distortion.
const canvas = document.querySelector('#hero');
const ctx = canvas.getContext('2d');
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = innerWidth * dpr;
canvas.height = innerHeight * dpr;
draw(current);
}
function drawCover(img) {
const cw = canvas.width, ch = canvas.height;
const s = Math.max(cw / img.naturalWidth, ch / img.naturalHeight);
const w = img.naturalWidth * s, h = img.naturalHeight * s;
ctx.drawImage(img, (cw - w) / 2, (ch - h) / 2, w, h);
}
Step 4: Map scroll position to a frame index
Give the section a tall runway, typically two to three viewport heights, and pin the canvas with position: sticky (or fixed). Progress is the scrolled distance divided by the runway length, and the frame index is progress times the frame count.
<section class="runway">
<canvas id="hero"></canvas>
</section>
.runway { height: 300vh; position: relative; }
#hero { position: sticky; top: 0; width: 100vw; height: 100vh; display: block; }
let current = 0;
function onScroll() {
const r = document.querySelector('.runway').getBoundingClientRect();
const progress = Math.min(1, Math.max(0, -r.top / (r.height - innerHeight)));
current = progress * (TOTAL - 1);
}
Step 5: Draw with requestAnimationFrame and cross-fade between frames
Drawing inside requestAnimationFrame keeps work in sync with the display. Easing the displayed value toward the target gives inertia, and blending the two nearest frames removes any visible stepping when the frame count is low.
let shown = 0;
function tick() {
shown += (current - shown) * 0.12; // ease toward target
const i = Math.floor(shown), t = shown - i;
const a = frames[i], b = frames[Math.min(i + 1, TOTAL - 1)];
if (a) {
ctx.globalAlpha = 1; drawCover(a);
if (b && t > 0.001) { ctx.globalAlpha = t; drawCover(b); }
}
requestAnimationFrame(tick);
}
addEventListener('scroll', onScroll, { passive: true });
addEventListener('resize', resize);
preload(); resize(); tick();
Step 6: Smooth the scroll itself with Lenis
Mouse wheels scroll in jumps. A library such as Lenis normalizes wheel input into eased scrolling so the film feels fluid. Initialize it once and call lenis.raf(time) from the same animation loop. It is optional, but it is the difference between a good and a premium feel.
Scroll sequence in WordPress, Framer and no-code tools
You do not have to hand-code a scroll sequence if you use a site builder. In WordPress, a scroll sequence plugin lets you upload the frames, set the section height and add text over the animation; create the sequence from your video, upload the image frames to the media library and the plugin handles preloading and scroll mapping. In Framer you can build a scroll-driven image animation with a code component that draws frames to a canvas, or with scroll-linked animations over a stack of images. The manual canvas method in this guide is the faster option when performance matters, because you control frame loading, mobile sets and the draw loop yourself, and it works on any site, plugin or not.
How to add text overlays to a scroll animation
Place normal HTML text above the sticky canvas and fade it in and out by progress ranges, for example headline at 0 to 25 percent and a second block at 40 to 65 percent. Because the text stays in the page, search engines and screen readers can read it.
Common uses for scroll sequences
Product reveals, hardware teardowns, ecommerce hero sections, portfolio intros, storytelling pages and cinematic landing pages all use this technique. The immersive experience gives visitors a reason to keep scrolling.
Step 7: Mobile, memory and accessibility
- Serve a lighter set to phones. Choose the 960 wide, lower fps set when
matchMedia('(max-width: 768px)')matches. - Cap DPR at 2. A 3x canvas multiplies memory with no visible gain.
- Respect reduced motion. If
prefers-reduced-motion: reduceis set, draw one representative frame and skip the scroll mapping. - Keep text real. Headlines and copy stay in HTML over the canvas so search engines and screen readers can read them.
Troubleshooting: canvas not showing or not working
- Canvas is blank: the first frame has not loaded. Draw only when
frames[i]exists, and draw frame 0 as soon as it arrives. - Canvas looks blurry: you did not multiply width and height by the device pixel ratio.
- Animation stutters: frames are too large or decoded late. Use WebP, set
img.decoding = 'async', and lower the mobile frame set. - It does not pin: an ancestor has
overflow: hidden, which breaksposition: sticky. Move the overflow rule or switch to fixed positioning. - Works in dev, fails in production: check asset paths and that the CDN serves the frames with long cache headers.
Core Web Vitals and SEO
Keep the largest contentful paint fast by showing a static poster frame immediately, then upgrading to the canvas. Reserve the canvas height with CSS to avoid layout shift, and keep real HTML headings on top. See Core Web Vitals for animated websites for the full checklist.
Live examples and ready-made source
Every ScrollHaus website is built on this exact pipeline, so you can scroll a working version and read how it behaves. For a wider overview of the category, see scroll animation websites, the animated hero section patterns and the guide to making an animated website with AI.
Explore animated website templates at Scrollhaus →
Need help with a template, account, or project? Email admin@scrollhaus.ai.