โ† All guides

Apple-style image sequence scroll animation in WordPress

Apple's product pages have a trick people want to copy the moment they see it: you scroll, and the product turns, opens up or comes apart โ€” smoothly, and exactly as far as you scrolled. It is not a video. It is a series of still images, frames, drawn one after another onto a canvas, with the scroll position choosing which one. This guide covers how it works, how to make the frames, the code to build one in WordPress, and what to watch so it does not cost your visitors more than it shows them.

How it works

There are three parts:

  1. A tall section. Its height is the scroll distance: the taller it is, the slower the animation plays. Three or four screens is typical.
  2. A canvas that stays put. Inside the section, a canvas with position: sticky stays on screen while the section scrolls past.
  3. Scroll progress picks the frame. How far through the section the visitor has scrolled, from 0 to 1, times the number of frames, is the frame to draw.

Why frames and not a video? A video can follow the scroll too, but browsers jump to the nearest keyframe when they seek, so an ordinary video stutters or lags when it is scrubbed. Frames are exact: frame 57 is always frame 57. The price is weight โ€” every frame is an image to download โ€” and most of what follows is about keeping that price down.

Making the frames

Start from a short video, such as a 3D render or a product on a turntable, and cut it into images with ffmpeg:

mkdir frames
ffmpeg -i input.mp4 -vf "fps=30,scale=1920:-2" -c:v libwebp -quality 80 frames/hero_%04d.webp

That writes hero_0001.webp, hero_0002.webp and so on: 30 frames for every second of video, 1920 pixels wide, as WebP. Some numbers to aim for:

Upload the frames to the Media Library together. WordPress keeps their file names, so hero_0001.webp ends up at an address such as /wp-content/uploads/2026/10/hero_0001.webp โ€” open one in the library to see yours. WordPress also makes its usual smaller sizes of every frame, which turns out to be useful for phones.

The code

Here is a minimal version that works as it is. The markup goes on the page, in an HTML block or your page builder's HTML widget. Give the canvas the size of your frames:

<div class="frames">
	<canvas class="frames-canvas" width="1920" height="1080"></canvas>
</div>

The CSS makes the section tall and keeps the canvas on screen:

.frames {
	height: 300vh; /* the scroll distance */
	background: #000;
}

.frames-canvas {
	position: sticky;
	top: 0;
	display: block;
	width: 100%;
	height: 100vh;
	height: 100svh;
	object-fit: contain;
}

The JavaScript draws the frame that matches the scroll. Set count to the number of frames and src to where they are:

const section = document.querySelector('.frames');

if (section) {
	const canvas = section.querySelector('canvas');
	const context = canvas.getContext('2d');
	const count = 120;
	const src = (i) => `/wp-content/uploads/2026/10/hero_${String(i + 1).padStart(4, '0')}.webp`;
	const frames = [];
	let shown = -1;

	const ready = (i) => frames[i] && frames[i].complete && frames[i].naturalWidth > 0;

	const draw = () => {
		const rect = section.getBoundingClientRect();
		const distance = rect.height - window.innerHeight;
		const progress = distance > 0 ? Math.min(1, Math.max(0, -rect.top / distance)) : 0;
		const wanted = Math.round(progress * (count - 1));
		// Until the wanted frame arrives, draw its nearest neighbour that has.
		for (let d = 0; d < count; d++) {
			const i = [wanted - d, wanted + d].find(ready);
			if (i !== undefined) {
				if (i !== shown) {
					context.drawImage(frames[i], 0, 0, canvas.width, canvas.height);
					shown = i;
				}
				return;
			}
		}
	};

	// Every eighth frame first, then the gaps, so the whole range is covered early.
	for (const step of [8, 4, 2, 1]) {
		for (let i = 0; i < count; i += step) {
			if (!frames[i]) {
				frames[i] = new Image();
				frames[i].onload = draw;
				frames[i].src = src(i);
			}
		}
	}

	let queued = false;
	window.addEventListener('scroll', () => {
		if (!queued) {
			queued = true;
			requestAnimationFrame(() => {
				queued = false;
				draw();
			});
		}
	}, { passive: true });
}

Two details in it do most of the work. It redraws only when the frame changes, so scrolling within one frame costs nothing. And it loads every eighth frame first, then the frames in between, drawing the nearest frame that has arrived โ€” so the animation works from the first second, coarse at first and smooth a moment later, instead of waiting for frame 120.

In StyleScript, the free plugin for your own CSS and JavaScript, the CSS goes in the CSS box and the script in the JavaScript box, loaded in the footer. The if (section) line keeps the script harmless on every page that has no animation.

What the minimal version leaves out

A version for a real site needs a few more things, and each one is about the visitor on a phone:

The no-code version

StyleScript Pro, the add-on to the free plugin, has this as a ready-made effect: Image Sequence Animation On Scroll, set up in a form under Appearance โ†’ StyleScript โ†’ Effects. You select all the frames in the Media Library at once โ€” they play in file-name order โ€” or give the address of numbered files with {0001} where the number goes. You place it with a shortcode or the StyleScript effect block, or in an element already on the page.

It does the list above: frames load once the effect is near the screen, in passes, with the nearest frame drawn until each one arrives; phones get every second, third or fourth frame (your choice) in WordPress' smaller size; the canvas is at most twice the screen's pixels and redrawn only on a new frame; visitors who prefer less motion see a still image; and the space is reserved before anything loads, so nothing on the page moves. It tells you when the frames together weigh more than we recommend, previews on any page before it goes live, and if you ever want the code for yourself, Detach hands it over as readable code.

Or a video that scrubs

When the frames would be too heavy, one video can do the same job โ€” if it is encoded for it. Seeking is only smooth when keyframes are close together, so the video needs a keyframe every few frames, no sound, and its index at the start of the file:

ffmpeg -i input.mp4 -an -vf "scale=-2:1080" -c:v libx264 -crf 23 -preset slow -g 5 -keyint_min 5 -sc_threshold 0 -pix_fmt yuv420p -movflags +faststart scrub.mp4

StyleScript Pro's Video Scrubbing On Scroll effect plays a video like that as visitors scroll, smooths the seeking, and shows its poster image to visitors who prefer less motion.

Keep your custom code in one place

StyleScript holds your CSS, JavaScript and header/footer code apart from the theme, prints each where it belongs, and keeps every version you save.

See what StyleScript does ย Pricing