Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 12.4Intermediate ~20 min · shot 4 of 7

Scroll Timelines in JavaScript

In one line

new ScrollTimeline({ source }) and new ViewTimeline({ subject }) make the same timelines from JavaScript, and element.animate(keyframes, { timeline, rangeStart, rangeEnd }) plays an animation on them. Use it for animations created at run time, and test for support first.

0/7 · 0%

Think of it like this

A thermostat with a dial and a remote. The dial on the wall (CSS) is always there and does one job. The remote (JavaScript) can do the same job from anywhere, at any moment, for any room you point it at.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

ScrollTimeline
The JavaScript class for a scroll progress timeline. Its source is the scroller, its axis the direction, its currentTime a percentage.
ViewTimeline
The class for a view progress timeline. Its subject is the element followed. It also has startOffset and endOffset.
rangeStart / rangeEnd
Options of animate() (and properties of the animation) that choose the part of the timeline used, like animation-range in CSS.
feature detection
Checking that the browser has a feature ("ScrollTimeline" in window) before using it, instead of assuming.

Step by step

01Create the timelines

"The same timelines from code" tests for both classes, makes a ScrollTimeline and a ViewTimeline, and plays one animate() on each.

02Read the range back

The card's rangeStart prints as entry 0%, and the bar's duration option is auto.

03Remove the scroller

In "What happens when the source goes away": scaleX 0.60 half way, 0.10 and progress null when removed, progress 0% when it is back.

04Cancel

cancel() ends the animation: the meter returns to its own style and no animation is left.

Try it yourself

  1. 1

    Give the view timeline an inset

    Measured on a 100 px scroller with a 40 px subject starting at 300 px: new ViewTimeline({ subject }) runs from 200 px to 340 px. With inset: "20px" it runs from 220 px to 320 px. With the list form inset: ["auto", CSS.px(20)] it is 220 px to 340 px: only the end inset, so the animation starts 20 px later and ends where it did.

Code & diagrams

The same timelines from code

Scroll the box: the bar follows the scroll, and the card fades in over its entry. The whole script is inside a feature test.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

progress timeline: ScrollTimeline · axis block · source is the scroller: true
journey timeline: ViewTimeline · subject is the card: true
card range starts at: entry 0%
bar duration option: auto
What happens when the source goes away

The meter's own style is scaleX(0.1). The animation goes from 0.2 to 1. The buttons repeat the removal by hand.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

scrolled half way: scaleX 0.60, timeline progress 50%
scroller removed: scaleX 0.10, timeline progress null, animation running
the timeline still points at it: true
scroller back: scaleX 0.20, progress 0%, scrollTop 0
after cancel(): scaleX 0.10, 0 animations left

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Remove the scroller and keep going

In "What happens when the source goes away" remove the line run.cancel(); and read the last console line.

terminal
$ Run and read the last line
── what you'll see ──
The last line reads `after cancel(): scaleX 0.20, 1 animations left`: the animation is still alive and moves the meter again whenever the scroller scrolls.
From a real run, with the cancel line deleted: the meter is at 0.20 and one animation remains.

Break #2

Use ScrollTimeline without checking

In "The same timelines from code" add delete window.ScrollTimeline; as the first line of the script, and change the if line to if (true) {.

terminal
$ Run it
── what you'll see ──
`ReferenceError: ScrollTimeline is not defined`, and the script stops before any animation starts.

Myth vs fact

Myth

A JavaScript scroll timeline is the same as a scroll listener that sets a style.

Fact

The browser still applies the animation. Your code only creates it. No code runs per scroll event.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    One timeline object can drive many animations: create new ScrollTimeline({ source }) once and pass the same object as timeline to every animate() call. They are then guaranteed to agree.

Remember this

  1. 1

    The problem this solves: CSS ties an animation to a timeline when the stylesheet loads. But some animations are created by code: a card the user just added, a list built from data, a range that depends on a measured height. For these you need to create the timeline and the animation yourself.

  2. 2

    The two classes. new ScrollTimeline({ source, axis }): the source is the scroller (default: document.scrollingElement), the axis "block" (default), "inline", "x" or "y". new ViewTimeline({ subject, axis, inset }): the subject is the element to follow; the inset is a string like "20px" or a list of CSS.px(...) values. The first example makes one of each.

  3. 3

    Playing an animation on one. element.animate(keyframes, { timeline, fill: "both" }). Leave out duration: it stays auto, the whole range (the example prints auto). Choose the part of a view timeline with rangeStart and rangeEnd ("entry 0%"). animation.rangeStart has a rangeName and an offset. A bad range throws TypeError: … Animation range must be a name <length-percent> pair. cancel() and getAnimations() work as usual, and the timeline has no destroy(): cancel what you started.

  4. 4

    Always test for support. "ScrollTimeline" in window is the check (CSS has CSS.supports("animation-timeline", "scroll()")). Without it, a browser lacking the class fails with ReferenceError: ScrollTimeline is not defined (measured by deleting the class) and the script stops. Decide the fallback first: nothing (a progress bar), an IntersectionObserver (a reveal, Topic 12.5), or a polyfill that imitates the timelines in JavaScript (it cannot run on the compositor).

  5. 5

    A timeline whose source disappears. Remove the scroller and the timeline goes inactive: progress null, the animation still running but with no effect, the element back to its own style, and timeline.source still pointing at the removed element. Put the scroller back and the progress restarts from 0%, because a re-inserted element's scroll position resets (second example).

Explain it without notes

01

When would you create a scroll timeline in JavaScript rather than write animation-timeline in CSS?

02

What does a ScrollTimeline do when its source element is removed from the page?

Practice

01

Write a function follow(scroller, bar) that returns a cancel function, and does nothing (returns a no-op) if scroll timelines are missing.

02

A row of cards is added after load. Give each one a reveal on its own view timeline, from entry 0% to entry 100%.

Done when you can

  • You can create a ScrollTimeline and a ViewTimeline and play animate() on them

  • You leave duration out and set rangeStart and rangeEnd as strings

  • You test for support, and decide the fallback first

  • You cancel animations when their element is removed