Skip to content

Playback settings & triggers

Change how an animation plays — its length, loops, direction, what starts and stops it, what the end leaves behind — without going back to the editor. Everything about when and how it plays lives in one place, the document’s animator.timeline block, and every player lets you override it at runtime from component props or the player options. This page is the reference for those fields and the overrides.

The editor writes the same block from its playback panels; if you only want to set the defaults there, see Set default playback settings & triggers.

A document with its animator block. (The comments are explanatory; JSON does not allow comments, so a real file has none.)

{
"type": "svg",
"viewBox": "0 0 400 400",
// Everything about WHEN and HOW the animation plays lives here
"animator": {
"timeline": {
"outTime": 2000,
"iterations": "infinite",
"iterationDirection": "alternate",
// Plays as soon as half of it is on screen; pauses while it is scrolled away
"actions": { "outOfView": "pause" }, "inViewThreshold": 0.5,
"actionTriggers": { "start": "load" }
}
},
"children": [
{
"type": "circle",
"id": "ball",
"cx": 0, "cy": 0, "r": 40, "fill": "#0087ff",
"animate": {
"transform": {
"keyframes": [
{ "time": 0, "value": { "translate": [200, 60] }, "easing": [0.33, 0, 0.67, 0.33] },
{ "time": 2000, "value": { "translate": [200, 340] } }
]
}
}
}
]
}

The same bouncing ball as in the web player, now two seconds per bounce and waiting until half of it has scrolled into view.

Three things make an animation go: the triggers (the user hovers or clicks), a component’s play / pause props (your app’s state), and the player API (play(), pause(), stop() from your code). All three call the same built-in actions, so the format writes down how each action looks exactly once, in the timeline’s actions block, and all three honour it. The two blocks are not mirror images of each other:

  • actionTriggers keys are the requests a page event can make — start, stop, pause (resume is derived unless the file names it).
  • actions keys are the moments whose outcome is configurable — start, stop, finish, outOfView.
  • start and stop are in both because each is both a request and a moment; pause has one behaviour (freeze), so there is no actions.pause; finish and outOfView are not page events, so they have no list.
actiondoeshow it looks (actions)requested by (actionTriggers)
startstopped or finished → playingstart.behavior: continue · restart · jumpToFinishedstart
stopplaying or paused → stoppedstop.behavior: reset · rewind, stop.completeIterationstop
pause / resumefreeze, and carry onnothing to configurepause; resume — derived unless the file names it
finishthe run ends by itselffinish.behavior: hold · reset · rewind— (the player)
outOfView / wakenobody can see it / seen againoutOfView.behavior: pause · continue · reset— (the view sensor: a condition, not an event)

So player.stop() on a looping heartbeat with actions: { stop: { completeIteration: true } } finishes the beat and then rests — and so does a hoverOut listed in actionTriggers.stop, and so does play={false} on the React component. One vocabulary, learnt once.

The timeline — what advances the playhead

Section titled “The timeline — what advances the playhead”

animator.timeline says what drives the animation’s progress, exactly like a Web Animations API timeline. Its type picks the kind, mirroring WAAPI’s DocumentTimeline / ScrollTimeline / ViewTimeline:

timeline.typeThe playhead follows…
time (default — may be omitted)wall time — something starts it (the actionTriggers), and it has the playback dynamics below
scrolla scroll container’s offset — Scroll-driven playback
viewthe SVG’s journey through the viewport — Scroll-driven playback

Each type carries only the fields that mean something for it — a scrubbed timeline has no actionTriggers, no actions and no delay, and the format gives them no slot there. Omitting timeline entirely means a time-driven timeline with every default.

Timing, the playback dynamics, how the animated attributes get updated and at what rate ALL live in the timeline. animator itself keeps only what is not playback: the lookup tables (definitions, bindings).

FieldValuesDefaultMeaning
timeline.inTimems0where the content WINDOW begins. Keyframe times are absolute content ms
timeline.outTimemsmax keyframe time…and where it ends — one pass plays inTime → outTime. The editor always writes it
timeline.playDurationmsoutTime − inTimethe window remapped to WALL time: one pass takes this long. Set it to play the same content faster or slower
timeline.frameRatefpsuncappedtarget rate for the player’s frame loop only — a parameter of the engine timeline.engine selects, so it sits beside it
timeline.engineauto · native · jsautohow the animated attributes get updated — Engine
timeline.delayms0waited whenever a run starts from the beginning — the first play, a restart, a play after a reset or an end — never on a resume from a paused position. A negative value skips ahead instead: -500 starts right away from the frame at 0.5 s
timeline.iterationsnumber · "infinite"1how many times the whole document timeline repeats
timeline.iterationDirectionnormal · reverse · alternate · alternateReversenormaleach iteration’s direction; alternate ping-pongs on every other iteration. reverse applies to a single pass too — still one iteration

Per-property loops vs iterations. There are two kinds of repetition, and they work at different levels. iterations repeats the whole document — every element, from the first keyframe to the last. A single property can also loop on its own: a segment of its own keyframes repeats until it fills the document’s duration, while everything else plays through once (see JSON format → Per-property loops). The property loop is applied first, when the document is prepared; iterations then repeats the result. So both can be used at once, and one runs inside the other: a wheel whose rotation loops, inside a document set to infinite iterations, keeps spinning during every iteration.

Three time axes appear in these names — all in ms, as everywhere on this API:

AxisNames
content ms — one pass of the content windowinTime, outTime, the handle’s getDuration() (× iterations for the whole run)
wall ms of one passplayDuration
ms into the whole run, every iteration includeddelay, the components’ showAtTime, the handle’s getCurrentTime() / setCurrentTime()

The actionTriggers block — inside the time timeline — names the user interactions that call the built-in actions. One list per request — start, stop, pause, and resume when the file names it — over one open vocabulary of events:

"timeline": {
"actions": { "stop": { "completeIteration": true } },
"actionTriggers": { "start": ["hoverIn", "click"], "stop": "hoverOut", "pause": "click" }
}

Read cold: start on hover-in or click, stop (after the beat) on hover-out, a click while playing pauses and the next click carries on. Each list is a single event or an array of them — the house style of animate’s references — and the editor writes the shortest exact spelling.

ListCallsActs while the player is…Editor label
startstart, per actions.startstopped (or finished)Play on
stopstop, per actions.stopplaying or pausedStop on
pausepause — freezes the current frameplayingPause on
resumeresume — carries on from there. Derived unless the file names it (below)pausedResume on (shown, not edited)

Two clicks make two toggles — spelt with the lists themselves, no extra key:

Click togglesactionTriggersA click while…
start / stop{ "start": "click", "stop": "click" }stopped starts it; playing or paused stops it
play / pause{ "start": "click", "pause": "click" }stopped starts it; playing pauses it; paused resumes it
EventFires when…
loadthe document is ready — the only event the visibility gate may hold back: nobody interacted, so it plays once enough of it is on screen
hoverInthe pointer enters the element
hoverOutthe pointer leaves it — only after it entered (a pointer already over the element at load does not count)
clickthe element is clicked (a tap on React Native)

The vocabulary is open: a later interaction (keyboard, focus, an app event) is a new word in the same lists, never a new key.

Each list names only the events that make sense for it; any other event is ignored, with a warning (PX1110).

ListloadhoverInhoverOutclickWhy
start✓✓—✓starting when the pointer leaves makes no sense
stop——✓✓stopping on load, or when the pointer arrives, makes no sense
pause—✓✓✓a marquee pauses on hover-in, a hover animation on hover-out
resume—✓✓✓derived unless the file names it — see below

What resumes a pause — derived unless named

Section titled “What resumes a pause — derived unless named”

resume need not be written: when the file does not name it, the player derives it from start, stop and pause. A paused animation then resumes on

  1. the counterpart of each pause event — hoverIn ↔ hoverOut, click → click (load has none), and
  2. the start events that do not also stop it — so a click that starts and stops ("start": "click", "stop": "click") stops a paused animation, as the lists say.
actionTriggersResume onPlays as…
{ "start": "load", "pause": "hoverIn" }hoverOut (and load, which has fired by then)a marquee: runs on load, holds while the pointer is over it, carries on when it leaves
{ "start": "hoverIn", "pause": "hoverOut" }hoverIna hover animation that holds where it is when the pointer leaves
{ "start": "click", "pause": "click" }clickthe play / pause click toggle: start → pause → resume → pause …
{ "start": "click", "stop": "click" }—the start / stop click toggle: one click starts it, the next stops it — also while paused

A resume list the file does name is honoured as written. Where it differs from the derivation — it names an event the derivation would not, or leaves out one it would (that pause then waits for start or stop) — the player notes it (PX1111), so a surprising resume is no mystery.

An event may be listed under several lists. The player’s state picks the list it is read from — start when stopped, pause when playing, the derived resume when paused, stop when playing or paused — and every action checks the state as it was when the event arrived.

  • { "start": "click", "pause": "click" }: a click while stopped starts, while playing pauses, while paused resumes.
  • Add actions: { start: 'restart' } and a click while stopped restarts, while a click while paused still resumes.

Two lists that react to the same event in the same state are a collision: the players report it (PX1109) and one of them wins.

actionTriggersCollision?What happens
{ "stop": "click", "pause": "click" }yes — stop with pausewhile playing, pause wins
{ "start": "load", "pause": "hoverIn", "stop": "hoverOut" }yes — stop with the counterpart of a pause eventwhile paused, hoverOut resumes rather than stops
{ "start": "click", "pause": "click" }nothe normal play / pause toggle — each list acts in another state
{ "start": "click", "stop": "click" }nothe start / stop click toggle — each list acts in another state
{ "start": "hoverIn", "stop": "hoverOut" }nostart on hover-in, stop on hover-out

Every warning the players give about a triggers block, by kind:

KindExampleThe player…Code
eventNotAllowed"stop": "load"ignores the event (in a file, the document repair drops it from the list first — PX1108)PX1110
collision"stop": "click", "pause": "click"lets pause win while playing, the resume while pausedPX1109
resumeExtra"start": "load", "pause": "hoverIn", "resume": ["hoverOut", "click"]honours the list as written; click is the event the derivation would not namePX1111
resumeMissing"start": "load", "pause": ["hoverIn", "click"], "resume": "hoverOut"honours the list as written; a pause by click then waits for start or stopPX1111

A document with no actionTriggers block starts on load — a document is designed to play. An empty block, "actionTriggers": {}, lists nothing: nothing starts it until your code calls play(). A list that is absent is simply not listed.

Pause and carry on need no setting. “Pause when the pointer leaves” is hoverOut listed under pause (hover-in resumes it — derived); “keep playing” is hoverOut not listed at all. That is what the editor does for you: ticking Hover in under Play on adds Hover out under Pause on, editable.

Actions — how start, stop and finish look

Section titled “Actions — how start, stop and finish look”

timeline.actions gives the built-in actions their settings — one slot per moment. behavior names a preset: one word for where the move ends and the path it takes there (an instant jump, playing on, playing back) — and a slot that says only behavior is written as the word itself: "stop": "rewind" is exactly "stop": { "behavior": "rewind" }. Every setting has a stated default, so a document that says nothing plays the way it always did: a start continues, a stop resets, the end holds. A key here names a moment; start and stop are requests too, so they appear in actionTriggers as well (Two blocks).

FieldValuesDefault
start.behaviorcontinue · restart · jumpToFinishedcontinue
stop.behaviorreset · rewindreset
stop.completeIterationtrue · falsefalse
finish.behaviorhold · reset · rewindhold
outOfView.behaviorpause · continue · resetpause — the visibility gate

pause, resume and wake have nothing to configure.

actions.start.behaviorA start (a start event, play={true}, play())…
continue (default)continues from the current position. At the start or the end, that is a fresh run from the beginning
restartstarts from the beginning every time — a splash that must replay on every click
jumpToFinishedlands in finished: the end pose with no motion — overlay art that appears instantly and retreats animated
actions.stop.behaviorA stop (a stop event, play={false}, stop())…
reset (default)jumps back to the state as before it started
rewindplays BACK to it — the VCR word, never confused with iterationDirection: reverse. The trash-can lid: opens on hover-in, closes animated on hover-out
actions.stop.completeIterationWHEN the stop takes effect
false (default)now
trueafter the current iteration — a heartbeat should not die mid-beat; a one-shot runs to its end first. Orthogonal to behavior: rewind after completion plays back from there — finish the motion, then undo it smoothly
actions.finish.behaviorWhen it ends by itself (every iteration played)…
hold (default)keeps the last frame showing
resetjumps back to the start state
rewindplays back to the start state

reset means ONE thing wherever it appears (the stop, finish and out-of-view presets): the state as before it started — the SVG as authored. A hand-authored file whose static attributes differ from its first keyframe can ask for the first frame instead with timeline.showWhenStopped, which governs the whole stopped state — before the first start, after reset(), after a stop or finish whose behavior is reset, and where a rewind lands (a paused animation shows its paused frame, a finished one its held frame):

timeline.showWhenStoppedWhat is shown while stopped
static (default)the SVG as authored
firstFramethe first frame’s values. The editor never writes it: in its files the two are the same picture

A one-shot document driven by a component’s play prop reads these settings too: play={true} is a start (the default continue plays to the end and holds there, per finish hold), play={false} a stop (reset snaps back, rewind plays back). The trash-can lid that closes animated is actions={{ stop: 'rewind' }}; overlay art that appears at once and retreats animated is actions={{ start: 'jumpToFinished', stop: 'rewind' }}.

Whether it may run — the visibility gate

Section titled “Whether it may run — the visibility gate”

An animation nobody can see does not play, whatever started it. The gate is a condition on every driver — the triggers, a component’s play prop and the API alike — not an event: once none of it is on screen the player performs the built-in outOfView moment, and once it is seen again wake, which carries on whatever was meant to run. outOfView has one setting; the two numbers that say when the sensor counts it as seen are plain timeline keys:

actions.outOfView.behaviorEffect while none of it is on screen
pause (default)pause where it is; it resumes when the element comes back
continuekeep playing — and start without waiting to be seen at all
resetjump back to the start, so the next entry replays it from the beginning

continue reads differently in the two slots that take it: under actions.start it means “from where it rests”, here “keep running”.

FieldDefaultMeaning
timeline.inViewThreshold0.5how much of the element must be on screen before it may run: 0 any part, 0.5 half of it, 1 all of it. Playback stops only at zero visibility, so an element resting on the boundary cannot flicker
timeline.inViewDebounce150and for how long, in ms. Scrolling straight past an animation therefore starts nothing; 0 starts the moment the threshold is met

wake needs no setting: if it was meant to run, it runs again — from the paused position after pause, from the start after reset; continue never stopped. A hidden browser tab counts as out of view. An element taller than the screen counts as fully visible once it fills the screen, so a threshold of 0.5 or 1 can always be met. Where nothing can measure visibility — a server render, a test environment without IntersectionObserver — the animation plays as if fully visible, rather than silently never playing.

To play an animation when it scrolls into view, nothing has to be set: start: load behind the default gate IS that behaviour — it waits on its first frame until half of it is on screen for 150 ms, plays, pauses when scrolled away, and carries on when it comes back.

  • Every player — web, React, Vue and React Native — honours the lists, the actions and the gate, with one exception: React Native has no hoverIn / hoverOut, because there is no hover on a touch screen.
  • Pre-rendered SVG + CSS animation + JS triggers honours all of them too. The editor writes a few lines of script into the file for this; no library is involved.
  • Pre-rendered SVG + CSS animation (no script at all) listens for load and, through CSS :hover, for hoverIn and hoverOut — hoverOut may pause or stop, not both. click and the visibility gate cannot be done in pure CSS and are dropped from the file; the actions keep their defaults (a class cannot restart keyframes or play them backwards). The editor marks every choice (CSS) or (JS) so you see what the script-free flavour keeps. See Pre-rendered SVG.
  • The CSS components (SvgCssAnimationPlayer in React and Vue) take the same actionTriggers object, actions limited to outOfView, and the two gate numbers as props, and read them with the same rules; rewind, restart, jumpToFinished and the iteration completion act as the defaults there, for the same reason.

Example: playback/override-web — pnpm example:docs, then open #playback/override-web. Example: playback/override-react — pnpm example:docs, then open #playback/override-react.

One document can play differently in each place you use it — the same file shown twice at two speeds, or a file that starts by itself everywhere except inside your own play/pause UI. Every player takes the same three overrides, and the file on disk is never modified:

prop / optionwhat it carries
the timingtimelineOptionscorrections to the file’s timeline block, minus its actionTriggers and actions — iterations, delay, playDuration, the gate numbers — as an object shaped like the file; whoever needs more edits the document
the actionsactionscorrections to the file’s timeline.actions, setting by setting — { stop: 'rewind' } changes how a stop looks and leaves the rest as the file has it
the triggersplayUsingActionTriggerstrue plays on the file’s own triggers; an object is the whole actionTriggers block to use instead. Lists left out are simply not listed, never the file’s values

Merge or replace — the one rule for the three:

timelineOptions, actionspatches — objects merge key by key, values replace, null deletes
a playUsingActionTriggers objectreplaces the file’s actionTriggers as a whole — its lists belong together, so switching hover → click would otherwise carry the hover-era lists along

The prop is timelineOptions, not timeline: it is not the file’s timeline object — two of its keys live on other props — and a prop that shares a file key’s name would promise the same shape. actions keeps the file’s name because it is a partial of that block.

Web player

<div id="box" style="width: 300px; height: 300px"></div>
import { createSvgAnimationPlayer } from '@pixodesk/svg-animator-web';
const a = createSvgAnimationPlayer({
src: '/bouncing-ball.json',
container: '#box',
timelineOptions: { iterations: 'infinite' },
actions: { stop: { completeIteration: true } }, // no `playUsingActionTriggers`: nothing starts until a.play()
});
a.play();
a.stop(); // finishes the current iteration, then rests — the actions said so

React / Vue / React Native — the same objects, as props:

// the file's triggers, looping forever
<SvgAnimationPlayer doc={doc} playUsingActionTriggers timelineOptions={{ iterations: 'infinite' }} />
// these triggers instead of the file's: starts on hover-in, plays back on hover-out
<SvgAnimationPlayer doc={doc} playUsingActionTriggers={{ start: 'hoverIn', stop: 'hoverOut' }}
actions={{ stop: 'rewind' }} />
// one click starts it, the next stops it
<SvgAnimationPlayer doc={doc} playUsingActionTriggers={{ start: 'click', stop: 'click' }} />
// driven by your state — the same actions, the same settings
<SvgAnimationPlayer doc={spinner} play={busy} actions={{ stop: { completeIteration: true } }} />
Objects merge, key by keytimelineOptions: { outTime: 2000 } changes the window end and leaves iterations and everything else as the file has them; actions: { stop: 'rewind' } leaves stop.completeIteration and the other actions alone
Values replacenumbers, strings and arrays are taken as given, never combined
null deletes{ timelineOptions: { delay: null } } removes the file’s delay, restoring what its absence means; { actions: { stop: null } } drops the file’s stop settings. This is the only way to get a default back, because there is no value that spells “unset”
actionTriggers and actions are not timeline keys herethe file nests them, the props lift them: only playUsingActionTriggers carries triggers and only actions the action settings, so every key is reachable exactly one way — timelineOptions={{ actionTriggers: … }} is a type error
Changing timeline.type starts overswitching between a clock and a scroll timeline keeps only inTime, outTime, iterations, engine and frameRate — the keys both kinds share. Clock-only keys (actionTriggers, actions, showWhenStopped, delay, playDuration, iterationDirection, the gate numbers) have no meaning on a scroll timeline and are dropped, with a console warning

Every player uses the same code from core. applyPlaybackOverride(doc, { timelineOptions, actions, playUsingActionTriggers }) is the one implementation the web player and the three components call: the timing and the actions merged (mergeAnimatorConfig’s rules, above), then — when playUsingActionTriggers is an object — the actionTriggers block set to it as a whole. Its input is a PxPlaybackOverrideInput (the timelineOptions and actions of PxPlaybackOverride plus playUsingActionTriggers), its answer a PxPlaybackOverrideResult: the new document, sharing every untouched subtree, and the merge’s warnings. applyAnimatorConfig(doc, patch) applies a patch to a whole document; mergeAnimatorConfig does the same one level down, on the animator block alone, and reports what it could and could not do in a PxAnimatorConfigMergeResult. A patch is a PxPatch of the wire type — every key optional at every depth, null deletes, arrays and primitives replace — so PxAnimatorConfigPatch patches the animator block, PxTimelinePatch the timing, typed from PxTimelineTiming (the timeline minus actionTriggers and actions), and PxActionsPatch the actions block (PxActions). withTriggers(doc, triggers) is the SET a PxActionTriggers block needs (an empty block and a replacement that drops keys are things a merge cannot say).

When you control a component with play, pause, showAtProgress or showAtTime, the component ignores the triggers saved in the file: core’s withoutTriggerDriver replaces them with an empty actionTriggers block, so nothing starts the animation by itself — the same patch the web player applies when playUsingActionTriggers is absent. Only playUsingActionTriggers mode follows the file’s triggers. The play / pause props call the built-in actions, so the file’s actions (and the actions prop) still say how each move looks. playerRef is not a control prop: the handle is filled in every mode and never changes which one you are in.

React, Vue and React Native decide this with the same core function, so they always agree. resolveControlMode looks at the control props you passed (PxControlProps) and returns the mode that wins (PxControlMode), plus a ready-made warning if you passed props from more than one mode. controlModeTakesOverTrigger then says whether that mode ignores the file’s triggers — true for every mode except playUsingActionTriggers.

For player authors — the same rules, exported

Section titled “For player authors — the same rules, exported”

Everything above is decided by functions in @pixodesk/svg-animator-core, so a player you write yourself cannot drift from the four shipped ones:

  • Defaults. PX_PLAYBACK_DEFAULTS is the one table of what an absent key means (start: 'load', the behaviors continue · reset · hold · pause, no iteration completion, showWhenStopped: 'static', 0.5, 150 ms). resolveTriggers(triggers) returns a PxResolvedTriggers — every list an array of the events it may name (triggerEventList normalises one), resume derived unless the file names it, an absent block { start: ['load'] }, an empty block nothing. resolveActions(actions) returns a PxResolvedActions — every setting defaulted; actionSettingsOf(slot) reads one slot however it is written (the word 'rewind' is exactly { behavior: 'rewind' }). resolveGate(view) returns a PxResolvedGate: the outOfView behavior and the two sensor numbers, defaulted.
  • The lists. PxTriggerList names them (start · stop · pause · resume); allowedTriggerEvents(list) is the table of events each may name (the schema is built from it); counterpartTriggerEvent(event) is the event that undoes another (hoverIn ↔ hoverOut, click → click, none for load); derivedResume(start, stop, pause) is the derived resume.
  • Schemas. PxActionTriggersSchema (fixed lists, each limited to its events) and PxActionsSchema, one fixed sub-schema per action — PxStartActionSchema, PxStopActionSchema, PxFinishActionSchema, PxOutOfViewActionSchema — each stating its defaults.
  • The event rule. triggerActionFor(triggers, state, event) — the player’s state (PxPlayState: stopped · playing · paused · finished) picks the list, and the answer is a PxTriggerAction: play, pause or stop. triggerListsEvent says which listeners to attach at all.
  • The warnings. triggerIssues(triggers) returns every problem with a block as a PxTriggerIssue — { kind, list, event, otherList? }, its kind a PxTriggerIssueKind (eventNotAllowed · collision · resumeExtra · resumeMissing); an empty array is a clean block. The web player reports them as PX1110, PX1109 and PX1111 (both resume kinds).
  • Where they live. The trigger functions (resolveTriggers … triggerIssues) and their types (PxResolvedTriggers, PxTriggerIssue) come from @pixodesk/svg-animator-core/engine; the consts PxTriggerList, PxTriggerIssueKind, PxPlayState and PxTriggerAction from the main entry.
  • The executor. One PxPlayExecutor per player performs every built-in action — start() per actions.start, stop() per actions.stop (preset and iteration completion), pause() / resume(), the state-rule play(), reset() (the unconditional reset-now), rewind(), and onNaturalFinish(), which performs actions.finish when the engine reports the end. It owns the one state engines cannot report: paused versus stopped. createPlayExecutor(api, config, raf) builds one over any PxPlayTarget (the slice of the API it drives) from a PxPlayExecutorConfig (the run’s geometry and the actions — executorConfigOf(view) reads them from the runtime view, a PxApiExtrasConfig); ensurePlayExecutor reuses the one an engine attached. composeApiExtras(api, cfg, raf) then completes an engine’s api (a PxApiExtrasTarget) with the composed members — play(), stop(), reset(), playOnce(), getDuration() — as PxApiExtras. applyDeclarativeAction drives a PxDeclarativeTarget (the public api, or a component’s proxy to it) from the declarative resolver’s actions.
  • CSS. fillModeOf(showWhenStopped, finishBehavior) is the animation-fill-mode the two settings spell — (static, hold) forwards, (firstFrame, hold) both, (static, reset) none, (firstFrame, reset) backwards — what the CSS export and the WAAPI engine hand to the browser.

In development. Scroll-driven playback is not finished yet: the fields below may change, and not every combination works in every player. Time-driven playback — the default — is not affected.

Instead of playing on a clock, the animation can follow the scroll position — the playhead moves as the user scrolls: scroll down and the animation goes forward, scroll back up and it goes backward, stop and it stays on that frame. This is the model of CSS scroll-driven animations. Choose Timeline → scroll in the editor’s playback panel, or set it in the document:

"animator": {
// The playhead follows the SVG's journey through the viewport instead of the clock;
// `outTime` is the keyframe span the scroll range maps onto.
"timeline": { "type": "view", "outTime": 3000, "range": { "start": { "phase": "entry", "fraction": 0 }, "end": { "phase": "exit", "fraction": 1 } } }
}

timeline: { "type": "view" } alone means “show the whole animation, first frame to last, as the SVG travels across the viewport — the scroll position, not the clock, decides which frame is on screen”; type: "scroll" follows the scroll container’s offset instead. The clock fields (actionTriggers, actions, delay, "infinite") have no slot in these timelines. The rest of the object tunes it:

timeline.ValuesMeaning
typeview · scrollprogress = the SVG’s journey across the scrollport, or the scroll container’s offset ratio
axisblock (default) · inline · x · ywhich axis; block = vertical in normal writing mode
sourcenearest (default) · rootfor type: scroll — the nearest scrollable ancestor, or the document
subjectparent · scroller · a CSS selectorfor type: view — whose journey is measured (default: the <svg> itself). parent is what makes a pinned section work
range.start / range.end{ phase, fraction }the slice of the journey mapped to 0–100 %; phase ∈ cover (default) · contain · entry · exit · entryCrossing · exitCrossing; fraction is a position within that phase, 0 = its start, 1 = its end
iterationsnumberthe animation repeats this many times across the range (finite only — "infinite" cannot map onto a range)
smoothingmscatch-up lag — the playhead eases toward the scroll position instead of snapping (smoother under momentum scrolling)
pintrue · { align, offset, distance }hold the canvas still on screen while scrolling moves the animation forward and back (position: sticky); align ∈ top/center/bottom, offset in px from the aligned position, distance in viewport heights creates the scroll travel
engineauto (default) · native · jswho computes progress and applies values — see Engine: auto/native use the browser’s ScrollTimeline where supported, js measures itself

Support: the web player (both engines, and therefore React and Vue), and the SVG + JS animation export. Not yet: the CSS export or React Native. The complete “scrollytelling” pattern is subject: "parent" + pin: true inside a tall section.

timeline.engine says how the animated attributes get updated — the same values on every timeline type:

ValueTime-driven timelineScroll / view timeline
auto (default)the Web Animations API — played by the browser itself, so it stays smooth even while the page is busy — with an automatic fallback to the player’s frame loop when the document animates something WAAPI cannot express (path morphing, gradient geometry, filters, text on a path, …)the browser’s own ScrollTimeline / ViewTimeline where supported; otherwise the player measures scroll progress itself and drives WAAPI (or the frame loop, if WAAPI declines the document)
nativeWeb Animations API onlythe browser’s ScrollTimeline / ViewTimeline driving WAAPI (where unsupported, the player measures progress instead — WAAPI stays)
jsa requestAnimationFrame loop that writes attributes every frame; honors frameRate; universal browser supportthe player measures scroll progress and applies values through its frame loop — identical everywhere

Leave it on auto unless you need a guarantee — for instance js for path morphing in Safari < 18.5. React Native ignores engine (playback is always native-driven).

Core exports the helpers the players decide this with, so a player you write yourself gets the same answer: resolveTimelineEngine turns the document’s setting into the engine that will actually run, isNativeForced says whether native was asked for outright, and mayUseNativeScrollTimeline whether a scroll timeline may be handed to the browser’s own ScrollTimeline.