Apple-Style Image Sequence on Scroll (Webflow + GSAP)

Advanced 30:39 webflowgsapscrolltriggercanvasimage-sequence

Recreate Apple's AirPods scroll animation in Webflow: extract video frames, host them, and drive an image sequence on a canvas with GSAP ScrollTrigger.

Key takeaways

  • The whole effect is an illusion: around 150 flat images swapped on a canvas as you scroll — no video tag, no WebGL, no Lottie.
  • A ~6-second source clip at 24fps is the sweet spot — enough frames (~144) for smooth motion, few enough to stay light.
  • Webflow silently drops visually-identical frames on upload, so compress them first — it nudges each frame's bytes and keeps static frames from vanishing as duplicates.
  • Webflow renames every asset on upload, so the CDN URLs are unguessable — the Data API is the only reliable way to fetch them in the right order without copying 144 links by hand.
  • A canvas carries no meaning to screen readers, so mark it aria-hidden and put the message in a visually-hidden paragraph beside it.

Video chapters

  1. 00:00 Intro
  2. 02:19 Frame extraction
  3. 05:48 Image compression
  4. 10:08 Getting the assets with the Webflow API and Postman
  5. 18:59 Implementing the scroll effect
  6. 24:05 How the code works
  7. 25:46 How to configure the effect
  8. 29:46 Outro

You can recreate Apple’s iconic AirPods Pro scroll animation in Webflow with nothing but a sequence of still images painted onto a canvas — no video, no WebGL, no Lottie. Apple pulled it off with 147 frames extracted from a roughly six-second clip, served one at a time as you scroll to fake perfectly fluid motion. This lesson rebuilds that exact effect from scratch, driven by GSAP ScrollTrigger.

The work splits cleanly into two halves. First, the asset pipeline: extract frames from a video, compress them, upload them to Webflow, and — the clever part — pull their URLs back out in order using the Webflow Data API instead of copying 144 links by hand. Second, the effect itself: a small, configurable script that draws the right frame onto a canvas as your scroll progress changes, controlled entirely by HTML attributes so you never have to touch the code.

Everything here is clonable, and the script is done for you. What actually matters is understanding the moving parts, so you can point this at your own animation and tune it to feel right.

How it works

The illusion. There’s no video playing. A <canvas> element is a blank, programmable surface, and the script draws a single frame onto it — then, as you scroll, it swaps that frame for the next one. Do that fast enough across ~150 images and your eye reads it as smooth motion. A source clip of about six seconds is ideal: at 24fps that’s roughly 144 frames, which is enough visual progression for a fluid scroll-linked animation while keeping the image count manageable.

The asset pipeline, and two quirks. You extract the frames with a free tool by setting a frame count and a distance between frames in milliseconds (for 6,000ms at 144 frames, that’s ~40ms apart). Before uploading, you compress them — and this isn’t only about weight. Webflow will silently discard images that are visually identical, even under different names, which happens constantly at the still start or end of an animation; compressing nudges each frame’s bytes just enough that the duplicates survive. Once uploaded, you let Webflow recompress everything to AVIF and move all the frames into a folder named exactly frames, so you can isolate them from the rest of your assets later.

The URL problem. Webflow renames every asset on upload — 00001.png becomes something unpredictable, and only the bit after the underscore is yours. So you can’t guess the CDN URLs. Instead you ask the Webflow Data API: one call lists your asset folders (which includes the IDs of the images inside each), a second call lists the assets themselves (with their full CDN URLs). You run both through Postman, paste the JSON responses into the generator tool, and it hands you a clean, ordered array of just the URLs inside your frames folder. Because the API returns only 100 assets per request, a project with ~150 frames needs a second call with an offset of 100 to fetch the rest — the tool merges the two batches for you.

The runtime. On the page, a tall section (e.g. 300vh) carries the two key attributes: fc-image-scrubbing="component" and fc-image-scrubbing-urls holding your array. Inside it, a position: sticky container at 100vh keeps the animation pinned to the viewport while you scroll through the section. The canvas fills that container. When the page loads, the script finds those two attributes, splits the URL list into an array, loads the images, watches scroll progress with ScrollTrigger, and draws the correct frame on every update — also handling high-DPI screens and responsive fitting. Because the canvas means nothing to a screen reader, it’s marked aria-hidden, and a visually-hidden paragraph beside it carries the same message in text.

How to use it

  1. Prepare your video. Keep it around six seconds and a reasonable resolution (1080p is plenty). Avoid alpha/transparency unless you truly need it — it makes frames heavy.

  2. Extract the frames. In the frame extractor, upload the video and set the frame count (e.g. 144) and the distance between frames in milliseconds (≈ total duration ÷ frame count, so ~40ms for a 6s clip). Extract, then download all.

  3. Compress them. Run the whole set through the compression tool — it keeps the original file names and handles a large batch at once. This both lightens the payload and prevents Webflow from dropping identical frames as duplicates.

  4. Upload to Webflow. Add every frame to the Assets panel, select all, and use Compress selected assets → AVIF to let Webflow shrink them further. Then create a folder named exactly frames and move all the images into it (the name matters — the tool relies on it).

  5. Fetch the URLs via the API. In the Webflow API docs, find list asset folders and copy the endpoint. In Postman, paste it, replace site_id with your project’s Site ID (Site settings → Overview), and under Authorization add a read-only API token as a Bearer token. Send it, copy the response, and paste it into the generator tool. Then change asset_folders to assets in the URL, send again, and paste that response in too; click generate array. For more than 100 assets, add a query parameter offset = 100, send, and paste the second batch to merge them.

  6. Build the layout. Create a section at 300vh and give it fc-image-scrubbing="component" plus fc-image-scrubbing-urls set to the copied array. Inside, add a container with padding 0, height 100vh, and position: sticky; top: 0. Drop a custom element inside, set its tag to canvas with width/height 100% and aria-hidden="true". Add a visually-hidden paragraph below it describing the animation for screen readers.

  7. Wire up the script and tune it. The clonable project includes the scrubbing script as an embed and loads GSAP + ScrollTrigger — make sure those are present. Then adjust the behavior with the attributes in the table above: fit (contain/cover), orientation-specific fit-landscape / fit-portrait, fps, and the ScrollTrigger start-point / end-point. Publish and scroll to see the sequence play in sync.

Configuration reference

Attribute Values Default What it does
fc-image-scrubbing component Marks the section (or div) that holds the animation. The script looks for this element and treats it as the container for the canvas sequence.
fc-image-scrubbing-urls comma-separated list of image URLs The ordered array of frame URLs, pasted straight from the generator tool. This is the essential one — no URLs, no animation.
fc-image-scrubbing-fit contain | cover contain How each frame is sized inside the canvas. contain shows the whole frame (possibly with space around it); cover fills the canvas entirely (possibly cropping the edges).
fc-image-scrubbing-fit-landscape contain | cover Overrides the fit value when the viewport is landscape — e.g. cover on desktop for a more immersive, edge-to-edge feel.
fc-image-scrubbing-fit-portrait contain | cover Overrides the fit value when the viewport is portrait — e.g. contain on mobile so the frame doesn't become oversized on a tall, narrow screen.
fc-image-scrubbing-fps number 24 The virtual playback rate. It doesn't change the render speed (scroll drives that) — it sets how finely the frames are spaced across the scroll range.
fc-image-scrubbing-start-point ScrollTrigger start syntax (e.g. 'top top') When the sequence should start as you scroll, using the same syntax as ScrollTrigger's start.
fc-image-scrubbing-end-point ScrollTrigger end syntax (e.g. 'bottom bottom') When the sequence should finish, using the same syntax as ScrollTrigger's end.

Resources

Frequently asked questions

Do I need Lottie or a video to make an Apple-style scroll animation in Webflow?
No. The effect is a sequence of still images drawn onto an HTML canvas and advanced by scroll position with GSAP ScrollTrigger. There's no video tag, no WebGL, and no Lottie file — just frames and a couple of attributes.
How many frames do I need, and how do I get them?
Around 150 for a ~6-second clip (6s × 24fps ≈ 144). Export the source at a reasonable resolution — 1080p is plenty — then use a free frame-extractor tool to slice it into numbered images, setting the frame count and the distance between frames in milliseconds.
Why compress the frames before uploading them to Webflow?
Two reasons. It shrinks the total asset payload, and it stops Webflow from silently discarding visually-identical frames as duplicates on upload — which happens often at the still start or end of an animation.
How do I get all the image URLs without copying 144 links by hand?
Use the Webflow Data API. List your asset folders to find the "frames" folder, then list the assets to get each CDN URL, and paste the JSON into the URL generator tool, which returns a clean, ordered array. For projects over 100 assets, add an offset parameter to pull the next batch.
Is the canvas accessible to screen readers?
The canvas is purely visual, so it's marked aria-hidden. The message it conveys — here, the animated logo — is provided as text in a visually-hidden paragraph next to it, so assistive technology still receives the content without seeing the pixels.

← Back to the course

Also part of these courses