Guides / Apple-style scroll animation
Technique guide ยท code tested and prices checked November 11, 2026

Apple-style scroll animation: how it works, with and without code

The effect people ask for by pointing at an Apple product page is a visual that moves as you scroll and stops when you stop. This page shows the three ways it is built, with working code for each, what each one costs in weight and effort, and how to get the same result without writing any code.

What the effect is

Scroll down a product page of this kind and the product does something: it turns, opens up, catches the light. Stop scrolling and it stops. Scroll back up and it runs in reverse. The section holding it stays pinned to the screen while this happens, then lets go.

Developers call this a scroll-linked or scroll-scrubbed animation: the scrollbar is the playhead. A scroll-triggered animation is different: it starts when a section arrives and then plays on its own clock. Our developer guide to scroll-driven storytelling covers the distinction.

Two public reference points, both checked on November 11, 2026:

  • The 2020 rebuild. On May 22, 2020 CSS-Tricks published Jurn van Wissen's Let's Make One of Those Fancy Scrolling Animations Used on Apple Product Pages. It recreates the hero of the AirPods Pro page, where light passes over the earbuds, as a sequence of 148 images drawn to a <canvas>, with the image chosen from the scroll position. In the author's words: "No complex WebGL scenes or advanced JavaScript libraries are needed." The article also notes that on a slow mobile connection Apple loaded a single fallback image in place of the sequence.
  • The page today. The HTML served at apple.com/airpods-pro on November 11, 2026 contains 15 <video> elements and no <canvas> element, with class names such as scroll-gallery, scroll-item-driver and scrub-scroll-container. We read the markup, not the scripts, so treat that as evidence of pinned, video-based sections and nothing more precise.

So there is no single Apple technique, only three building blocks. Scrollytelling.ai is not affiliated with Apple. Apple and AirPods Pro are named here only because that is how people search for this web technique.

Three ways to build it

All three share one skeleton: a tall section (the scroll runway) with a position: sticky child that stays on screen while the section scrolls past. Progress runs from 0, when the section's top reaches the top of the screen, to 1, when its bottom reaches the bottom. Every sample was run in Chrome at 1,280 px and 390 px wide before publishing.

1. An image sequence drawn to a canvas

Export the clip as numbered stills, load them, and draw the one that matches the scroll progress. This is the method in the CSS-Tricks rebuild, and the smoothest of the three, because showing a frame is a single image draw.

<section class="scrub">
  <canvas width="1280" height="720" role="img"
          aria-label="The product turning"></canvas>
</section>

<style>
  .scrub { height: 400vh; }  /* the scroll runway */
  .scrub canvas {
    position: sticky; top: 0;
    width: 100%; height: 100svh;
    object-fit: cover;
  }
</style>

<script>
  const section = document.querySelector('.scrub');
  const canvas = section.querySelector('canvas');
  const ctx = canvas.getContext('2d');
  const count = 120;  // frames/0001.jpg to 0120.jpg
  let shown = -1;

  function draw() {
    const r = section.getBoundingClientRect();
    const p = -r.top / (r.height - innerHeight);
    const i = Math.round(
      Math.min(1, Math.max(0, p)) * (count - 1));
    if (i === shown || !frames[i].naturalWidth) return;
    ctx.drawImage(frames[i], 0, 0,
      canvas.width, canvas.height);
    shown = i;
  }

  const frames = Array.from({ length: count }, (_, i) => {
    const img = new Image();
    img.onload = draw;
    img.src = 'frames/' +
      String(i + 1).padStart(4, '0') + '.jpg';
    return img;
  });

  let queued = false;
  addEventListener('scroll', () => {
    if (queued) return;
    queued = true;
    requestAnimationFrame(() => {
      queued = false;
      draw();
    });
  }, { passive: true });
</script>

The frames come from any clip with one ffmpeg command:

mkdir frames
ffmpeg -i clip.mp4 -vf "fps=24,scale=1280:-2" \
  -q:v 4 frames/%04d.jpg

To stay short, the sample requests all 120 frames at once. A real page loads the first few first.

2. Scrubbing a video with currentTime

Keep the clip as a video and set its playback position from the scroll. One file in place of 120 is the appeal.

<section class="scrub">
  <video src="scrub.mp4" muted playsinline
         preload="auto"></video>
</section>

<style>
  .scrub { height: 400vh; }
  .scrub video {
    position: sticky; top: 0;
    width: 100%; height: 100svh;
    object-fit: cover;
  }
</style>

<script>
  const section = document.querySelector('.scrub');
  const video = section.querySelector('video');
  let target = 0, sought = -1;

  // One seek at a time: never queue them up.
  function seek() {
    if (video.seeking || target === sought) return;
    sought = target;
    video.currentTime = target;
  }
  video.addEventListener('seeked', seek);

  addEventListener('scroll', () => {
    if (!video.duration) return;  // not loaded yet
    const r = section.getBoundingClientRect();
    const p = -r.top / (r.height - innerHeight);
    target = Math.min(1, Math.max(0, p)) * video.duration;
    seek();
  }, { passive: true });
</script>

Why it stutters. A normal web video stores a complete picture only at keyframes, often seconds apart, and describes the frames in between as changes. To show an arbitrary moment the browser must find the keyframe before it and decode forward. So setting currentTime starts a seek that finishes later (MDN: "Changing the value of currentTime seeks the media to the new time"), and scrolling asks for many per second. Three fixes:

  • Encode every frame as a keyframe. In ffmpeg, -g 1 sets the group-of-pictures size to one. The file is larger than a normal encode of the same clip.
  • Run one seek at a time. The sample waits for seeked, then jumps straight to the latest target.
  • Host it where range requests work. When we ran this sample from a basic local server with no HTTP range support, Chrome never left the first frame.
ffmpeg -i clip.mp4 -an -c:v libx264 -g 1 -crf 20 \
  -pix_fmt yuv420p -movflags +faststart scrub.mp4

3. CSS scroll-driven animations

The browser can tie an ordinary @keyframes animation to scroll position with no JavaScript. view-timeline gives the tall section a named timeline that follows its trip through the viewport, and animation-range: contain limits it to the stretch where the section fills the screen: exactly while the image is pinned.

<section class="stage">
  <img src="product.png" alt="The product, front view">
</section>

<style>
  .stage { height: 300vh; }
  .stage img {
    position: sticky; top: 0;
    width: 100%; height: 100svh;
    object-fit: contain;
  }

  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .stage { view-timeline: --stage; }
      .stage img {
        animation: turn linear both;
        /* these two go after the shorthand */
        animation-timeline: --stage;
        animation-range: contain;
      }
    }
  }

  @keyframes turn {
    from { opacity: .3;
           transform: scale(.7) rotate(-12deg); }
    to   { opacity: 1; transform: none; }
  }
</style>

The two anonymous forms cover the simpler cases: scroll() follows a scroll container, and view() follows an element's own trip through the viewport.

<div class="bar"></div>
<p class="card">Fades in on its way up the screen.</p>

<style>
  .bar {
    position: fixed; top: 0; left: 0;
    width: 100%; height: 3px; background: #fff;
    transform-origin: left;
    animation: grow linear both;
    animation-timeline: scroll(root);  /* page scroll */
  }
  .card {
    animation: rise linear both;
    animation-timeline: view();  /* its own trip */
    animation-range: entry 0% cover 40%;
  }
  @keyframes grow { from { transform: scaleX(0); } }
  @keyframes rise {
    from { opacity: 0; transform: translateY(2rem); }
  }
</style>

Browser support, checked November 11, 2026. MDN's compatibility data lists animation-timeline, scroll() and view() from Chrome and Edge 115, Safari 26 and Firefox 159. Firefox 159 shipped on October 27, 2026 with scroll-driven animations on by default, and MDN's animation-timeline page now labels the feature "Baseline 2026, newly available". Newly available also means Safari before 26 and Firefox before 159 ignore it, so the sample writes the finished state as the default and adds the motion inside @supports. Reference: MDN: CSS scroll-driven animations.

The limit. CSS animates properties of things already on the page: position, scale, rotation, opacity, clipping. It cannot play footage.

The trade-offs compared

RouteSmoothnessFile weightMobile behaviourEffort
Image sequence
on a canvas
Highest once frames are loaded: each scroll step is one image drawHeaviest: one image per frameDependable if phones get smaller and fewer framesMedium: export frames, write loading logic and canvas code
Video with
currentTime
Depends on the encode: stutters with sparse keyframes, good with a keyframe on every frameOne file. Small with a normal encode, larger with all keyframesLeast predictable: browsers are not obliged to seek instantly, and phones often do notLittle code, more encoding and device testing
CSS scroll-driven
animations
High: declarative, so the browser can run it off the main threadNone beyond the element you animateChrome and Edge 115, Safari 26, Firefox 159 and later. Older browsers show the static end stateLowest, but it cannot play footage
No-code
(Scrollytelling)
The canvas image-sequence method, set up for youFrames are extracted, optimised and served from a CDNWorks on phonesUpload a clip or generate one

A working default: the image sequence for the product, CSS for the text around it, and video scrubbing only when you control the encode and have tested on real phones.

Performance and accessibility rules that matter

  • Count the frames before you shoot. Weight is frame count multiplied by the size of one frame: 148 frames at 50 KB each is 7.4 MB. Keep the clip to a few seconds and size the frames to the largest box they appear in, no larger.
  • Preload the first frames. Show frame one as an ordinary image so something paints before any script runs, then fetch the rest ahead of the scroll.
  • Give phones a lighter version. Fewer and smaller frames, or a single still on a slow connection.
  • Set the runway length on purpose. Too short and the sequence flickers past. Too long and the visitor thinks the page has stopped responding. Size the pinned element with 100svh, as the samples do, so a phone's toolbar does not clip it.
  • Respect prefers-reduced-motion. Visitors who turn on reduced motion should get the content without the scrubbing: unpin the section and show a still.
  • Do not hijack the scrollbar. Read the scroll position, never override it: no preventDefault() on wheel or touch events and no eased scrolling of your own. The keyboard and the scrollbar keep working.
  • Keep real text in HTML. Headlines and copy belong in the page as text placed over the canvas, not baked into the frames. Text painted into a canvas or a video frame is invisible to screen readers and to search engines. Give the canvas a text label, as the first sample does.

The reduced-motion check for the canvas sample:

const calm = matchMedia(
  '(prefers-reduced-motion: reduce)').matches;

if (calm) {
  // no runway, no pin: show one still and stop
  section.style.height = 'auto';
  section.innerHTML =
    '<img src="frames/0060.jpg" alt="The product">';
} else {
  // load the frames and listen for scroll
}

The reasoning behind each rule is in scrollytelling design patterns. The code-level pitfalls (jank, layout thrashing, viewport units) are in the developer guide.

3D scrollytelling: two honest routes

A search for 3D scrollytelling means one of two things, and the first costs more than the second by a wide margin.

Real-time 3D. A model is rendered live in the browser with WebGL, usually through three.js (a JavaScript 3D library), and the scroll position drives the camera or the model. The visitor's device renders every frame. Choose it when visitors must be free to look around, change a colour or open a part. The bill is asset production, a graphics budget that has to hold on a mid-range phone, a designed loading state and a fallback for devices that cannot run it.

Pre-rendered 3D. The scene is rendered once, in advance, into frames or a video, and the page scrubs them on scroll with route 1 or 2 above. The visitor's device only shows pictures, and the camera path is fixed. The AirPods Pro hero that CSS-Tricks rebuilt is this kind: 148 images and no WebGL.

The rule from our design patterns guide: if the camera path is authored, pre-render it. Live sites of both kinds are in our scrolling websites list: one product page paints pre-rendered frames on a 2D canvas, and one annual report draws its graphics live in WebGL. Scrollytelling.ai does the second route. It scrubs a clip, including a 3D render you upload, and it does not build real-time WebGL scenes or custom shaders.

The no-code way: how Scrollytelling.ai does it

Scrollytelling.ai builds the image-sequence version for you. You upload a clip, or let AI generate a hero clip, and the clip is rendered to a sequence of image frames and scrubbed on a canvas tied to scroll, which avoids the stutter of seeking a real <video>. The frames are extracted and optimised for you, served from a CDN and loaded progressively, so the first frames paint straight away and the rest arrive as the visitor scrolls. It works on phones.

  1. Open the editor. Create a story and add a Scroll video section, or let AI generate the hero for you.
  2. Add your footage. Upload a clip or generate one. Steady, high-contrast shots scrub best, and a few seconds is enough.
  3. Publish. As a link on a scrollytelling.ai address, on your own domain (paid plans), as an embed on any site, in WordPress or on Shopify.

One limit: because the extracted frames can run to dozens of megabytes, the scroll video is built and hosted inside Scrollytelling, not exported as a standalone snippet.

This is a live story built with Scrollytelling. Scroll inside the frame: the zoom from the edge of the universe follows your scroll, stops when you stop and reverses when you scroll back up.

Universe to You, built without code: 999 frames scrubbed on a canvas. Scroll forward and back inside the frame, or open it full screen.

Before making an account, open the free scroll video playground: try it with your own clip, no signup. Paste a link to a video, adjust the text and colours, and copy the embed code.

What it costs

Plans as listed on our pricing section, checked November 11, 2026. Uploading your own clip and editing by hand do not use credits. Generating a clip or images with AI does.

PlanPricePublished storiesAI credits a monthAlso includes
Free$01, on a scrollytelling.ai address60All templates and widgets, built-in analytics, a small "made with" badge
Starter$12 a month, or $10 a month billed yearly ($120)10250Custom domain, badge removed
Pro$29 a month, or $24 a month billed yearly ($290)501,000Form submissions, AI Spotlight interactive sections, custom code embed
Pro+$59 a month, or $49 a month billed yearly ($590)Unlimited3,000Priority AI and support

No demo call, no sales contact, and you cancel in the app. Full details: pricing. For agency and freelancer prices, see scrollytelling cost.

On Shopify and WordPress

Shopify. The Scrollytelling app builds a scroll-driven page from a product, a collection or a campaign, including the scroll-scrubbed video, and publishes it to your storefront as a page, a blog article or a theme block. It does not edit your theme files, and product pages get add to cart built in. Building and previewing is free. Publishing starts on Starter at $12 a month, billed through Shopify. Details: scroll animation for Shopify.

WordPress. The official plugin embeds any published story in a post or page with a block, a shortcode, or by pasting the story link on a line of its own. The story runs in its own frame, isolated from your theme's CSS and JavaScript, and the plugin is free. Details: the WordPress plugin.

Frequently asked questions

How does Apple do its scroll animation?

Apple has not published a how-to, so the answer comes from reading its pages. In May 2020 CSS-Tricks rebuilt the AirPods Pro hero as 148 images drawn to a canvas and chosen by scroll position. On November 11, 2026 the HTML of the AirPods Pro page held 15 video elements inside pinned scroll components and no canvas element. Both are the same idea: a pinned section whose visual is tied to the scroll position.

Can I scrub a video on scroll?

Yes. Set the video element's currentTime from the scroll position. A normal encode stutters, because the browser has to decode forward from the last keyframe on every seek. Encode every frame as a keyframe, run one seek at a time, and host the file where HTTP range requests work. For the smoothest result, draw an image sequence to a canvas.

Is there a no-code tool for scroll animations, and what does it cost?

Yes. Scrollytelling.ai turns an uploaded or AI-generated clip into a scroll-scrubbed section without code. The Free plan is $0 with 60 credits a month and one published story. Paid plans are $12, $29 and $59 a month, or $10, $24 and $49 a month billed yearly. Prices checked November 11, 2026.

What is 3D scrollytelling?

A scroll-driven page whose visual is three-dimensional. Real-time 3D renders a model live in the browser with WebGL, usually through three.js, and the scroll moves the camera. Pre-rendered 3D renders the scene to frames or video in advance and scrubs them on scroll. The second is lighter, and it is how the AirPods Pro hero rebuilt by CSS-Tricks worked.

Does scroll animation hurt page speed?

It can. An image sequence weighs its frame count multiplied by the size of one frame. Keep the clip short, size frames to the screen, show the first frame as a normal image, load the rest progressively and give phones fewer frames. CSS scroll-driven animations add almost no weight, but they cannot play footage.

Can I add scroll animation to a Shopify store?

Yes. Either a developer codes it into your theme, or an app builds it. The Scrollytelling app for Shopify builds a scroll-driven page from a product, collection or campaign and publishes it as a page, blog article or theme block without theme edits. Building is free, and publishing starts at $12 a month billed through Shopify.

Try it with your own clip

Build one scroll-scrubbed page on the Free plan, or scrub a clip in the playground first. No code either way.

Create your story โ†’ Open the scroll video playground โ†’