Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 8.3Intermediate ~21 min · shot 3 of 6

Playback Control

In one line

An Animation is a small video player: play(), pause(), reverse(), finish() and cancel() steer it, currentTime seeks it and playbackRate changes its speed. Each has an edge case that the remote below lets you hit on purpose.

0/6 · 0%

Think of it like this

A video player. You can play, pause, drag the slider to any moment, watch at double speed, or run it backwards, and the video never has to be re-made. An Animation object is that player for motion.

Words you'll meet

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

playState
idle, running, paused or finished: where the Animation is in its life.
currentTime
How far the animation is along its own clock, in ms, including the delay. null when idle.
playbackRate
The speed multiplier: 1 normal, 0.5 half speed, negative backwards, 0 frozen.
seek
Jump to a time by setting currentTime.
pending
True for about one frame after you play, pause or change the rate, until the browser has applied it.
idle
An animation with no current time: never played, or cancelled.

Step by step

01Play, pause and read the state line

In "A remote for one animation", press Play, then Pause. The line under the buttons shows what the Animation itself reports: running | currentTime 812 | rate 1, then paused. The track on the timeline stays while paused: the animation still exists.

02Drag the scrubber

Drag the slider. The handler calls pause() and then sets currentTime. The dot follows your hand because every currentTime maps to one position. Press Play: it continues from where you left it.

03Reverse in the middle, then at the start

Press Play, and Reverse while the dot is moving: it turns round at once and runs back (a real run reversed at 500 ms and the dot went back down the same curve). Run the example again and press Reverse before Play: the dot jumps to the far end and runs back to the start. That is the auto-rewind rule. Press Reverse once more and the sign flips again, so the dot runs forward.

04Finish, then cancel

Finish puts the dot at the end (finished). Cancel sends it back to its own place (idle | currentTime null) and the track disappears: the animation was removed.

Try it yourself

  1. 1

    Seek, then play

    In "A remote for one animation", pause, drag the slider to 1500, then press Play. In a real run the dot sat at 191.6 px (the eased position at 75% of the time, not 75% of the distance), moved on and arrived at 220 px 500 ms later. A seek does not change the easing: it only chooses the moment.

  2. 2

    Half speed

    Choose rate 0.5 in the select, then press Play. The 2000 ms animation takes 4000 ms: in a real run the dot was at 28.4 px after 1000 ms, at 110 px (half way) after 2000 ms, and finished at 4000 ms. The track's duration still says 2000 ms: the rate changes how fast time passes, not the keyframes.

  3. 3

    Cancel versus finish

    Press Play, then Cancel: the dot jumps back to its own place at once and the track vanishes. Press Play and Finish: the dot jumps to the end and stays there, because the animation has fill: "both". Remove fill: "both" and Finish snaps back at the next frame.

Code & diagrams

A remote for one animation

One animation with fill: both so the dot stays at both ends. The buttons call the five methods, the slider seeks, and the select calls updatePlaybackRate(). The line under it prints the animation's own state every frame.

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.
Edge cases of playback

Chromium 153. Each line is one edge case, read straight from the Animation object.

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

finish(): finished, currentTime 1000
reverse() on a finished animation, right away: running, currentTime 1000, playbackRate 1, pending true
...and once it is ready: running, playbackRate -1
reverse() while paused at the start: currentTime 1000
playbackRate 0 after 3 frames: running, currentTime 0
currentTime 5000 on a 1000 ms animation: finished, currentTime 5000
finish() on an endless animation: InvalidStateError: Failed to execute 'finish' on 'Animation': Cannot finish Animation with an infinite target effect end.
currentTime = null while running: TypeError: Failed to set the 'currentTime' property on 'Animation': currentTime may not be changed from resolved to unresolved
right away, updatePlaybackRate(2) vs playbackRate = 2: 1 vs 2
once ready: 2 vs 2

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

Set the rate to 0 and wait

In "A remote for one animation", add the line anim.playbackRate = 0; after anim.pause();.

terminal
$ Press Play and read the state line
── what you'll see ──
running | currentTime 0 | rate 0
From a real run: the button "works" (the state says running) and the dot never moves. Run it before you touch the timeline's controls: once the timeline has taken over the clock, its Play put the rate back to 1.

Myth vs fact

Myth

cancel() ends an animation like finish() does.

Fact

finish() runs it to the end (and a fill keeps the end). cancel() removes it: the element goes back to its own style, currentTime becomes null, and anything waiting on finished is rejected (Topic 8.5).

Pro corner

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

  • ▸

    The animation's startTime is the moment on document.timeline it started. While pending it is null; after pause() it is null and currentTime holds the position. Changing currentTime while running recomputes startTime.

  • ▸

    animation.timeline can be a ScrollTimeline instead of document.timeline: then currentTime means scroll position, and the same methods work (Phase 12).

Remember this

  1. 1

    The problem this solves: the drawer must close when the user changes their mind half-way through opening it. If all you can do is cancel and start a new animation, the new one begins from the start and the drawer jumps. With a handle you call reverse() and the drawer turns round from where it is. The same handle drives demos, scrubbers and slow motion.

  2. 2

    The four states. playState is idle (never played, or cancelled), running, paused or finished. A newly started animation is also pending for one frame, until the browser has started it.

  3. 3

    The five methods. play() starts or resumes, and from finished restarts at the beginning. pause() freezes in place. finish() jumps to the end (the start, when playing backwards). cancel() throws the animation away: currentTime becomes null, the element returns to its own style, the animation goes idle and a cancel event fires. reverse() flips the sign of playbackRate (2 becomes -2) and plays.

  4. 4

    **Where reverse() surprises.** It is playbackRate = -1 plus play(), with an extra rule: if the animation is already at the end it plays back from there, and if it is at the *start* it first jumps to the end. In the console below, a paused animation at currentTime 0 reports currentTime 1000 right after reverse().

  5. 5

    **currentTime.** Milliseconds on the animation's own clock, including any delay. You can set it: anim.currentTime = 500 seeks. Past the end the animation is finished and keeps the number you gave (5000 on a 1000 ms animation). A negative number is before the start: the animation is paused or running there but shows nothing without a backwards fill. You cannot set it to null while it is running: that throws a TypeError. To seek a running animation, pause() first, or the clock keeps moving under your hand.

  6. 6

    **playbackRate. 1 is normal, 2 is double, 0.5 is half, a negative number plays backward. A rate of 0 is a trap:** playState stays running and currentTime never moves, so nothing looks wrong in the state and nothing moves on screen. Setting playbackRate takes effect at once. updatePlaybackRate(rate) first synchronises the position and applies the change on the next frame, so there is no jump when the animation is running off the main thread (MDN). playbackRate still shows the old value until ready resolves.

  7. 7

    Keyboard and accessibility. A scrubber should be a real <input type="range">, as in the remote below: arrow keys and screen readers work. Motion that plays on its own for over five seconds needs a visible pause control (WCAG 2.2.2, Topic 7.4).

Explain it without notes

01

How do you make a drawer turn round half-way, without a jump?

02

What is the difference between finish() and cancel()?

03

Why can a running animation not move?

Practice

01

Write toggle(anim) for a drawer: if it is running forwards, reverse it; if it is finished or idle, play it forwards; if it is running backwards, play it forwards again.

02

Write scrubTo(anim, fraction) that moves an animation to a fraction (0 to 1) of its duration, whatever its delay.

Done when you can

  • You can start, pause, reverse, finish and cancel an animation

  • You can seek with currentTime and explain why to pause first

  • You can name the edge cases: reverse at the start, rate 0, seek past the end

  • You know the difference between playbackRate and updatePlaybackRate()