React — @pixodesk/svg-animator-react
Use this in a React or Next.js app: drop in the component, pass it the JSON, and it renders the animation and controls its playback. It wraps the web player and renders the SVG with React itself, so it is SSR-safe and works in Next.js. Control it the way that suits your code — the document’s triggers, your own state, a ref, or a controlled position.
npm install @pixodesk/svg-animator-reactimport { SvgAnimationPlayer } from '@pixodesk/svg-animator-react';import animation from './animation.json';
export function Logo() { return <SvgAnimationPlayer doc={animation} playUsingActionTriggers />;}The component renders the root <svg> of the document; size it with className / style or
by sizing the parent (the SVG keeps its viewBox).
Control modes
Section titled “Control modes”Every way to make it play starts with play: playUsingActionTriggers (the document’s triggers, or ones
you pass), play / pause (your state), showAtProgress / showAtTime (your
position). Set props from more than one group and the highest-priority one wins —
showAtProgress / showAtTime → play / pause → playUsingActionTriggers — and the
component warns, naming both props and the winner. playerRef is not a mode: it is filled in
every mode and never changes which one you are in, so you can always call play() /
pause() / stop() yourself; playerRef with no control prop beside it is simply the static
mode, where nothing plays until you say so. React, Vue and React Native all resolve this the
same way, from one rule in core.
Nothing plays until a prop or option names the driver (playUsingActionTriggers, play,
showAt…). A file embedded with no code around it — the <div data-px-animation-src> tag, the
CSS wrapper, a standalone SVG file — has no other possible driver, so there the embedding is that
naming and the file plays as saved.
In every mode but playUsingActionTriggers the file’s own triggers are switched off — core’s
withoutTriggerDriver replaces them with an empty block, so nothing starts the animation by
itself (a note on the control props).
Imperative API (playerRef)
Section titled “Imperative API (playerRef)”Example:
react/imperative—pnpm example:docs, then open#react/imperative.
Pass a ref as playerRef. Once the component has mounted, the ref holds the playback API, so
any button, timer or effect in your app can start, stop, pause, or jump to any point in the animation.
import { useRef } from 'react';import { SvgAnimationPlayer, type PxAnimationPlayerHandle } from '@pixodesk/svg-animator-react';import animation from './animation.json';
export function Player() { const playerRef = useRef<PxAnimationPlayerHandle>(null); return ( <> <SvgAnimationPlayer doc={animation} playerRef={playerRef} /> <button onClick={() => playerRef.current?.play()}>Play</button> <button onClick={() => playerRef.current?.pause()}>Pause</button> <button onClick={() => playerRef.current?.stop()}>Stop</button> </> );}What playerRef.current gives you — the web API minus destroy / getRootElement / isReady,
because the component owns the element’s lifetime. It is core’s PxAnimationPlayerHandle, under that
one name in every framework package, so the three cannot drift apart. play(), pause() and
stop() take no arguments: they call the built-in actions, and how a start or a stop looks is
written once, in the timeline’s actions
(Actions),
which the handle follows exactly as the triggers and the play prop do.
interface PxAnimationPlayerHandle { isPlaying(): boolean; // true while running; false when paused, stopped or finished play(): void; // paused → resume; otherwise the configured start (`actions.start`) pause(): void; // hold the current frame stop(): void; // the configured stop (`actions.stop`) — after the current // iteration when its `completeIteration` is true reset(): void; // reset NOW, unconditionally — back to the start state jumpToFinished(): void; // jump to the end and hold it playOnce(): void; // ONE fresh pass from the start, whatever `iterations` // say — the verb behind "plays once on click"; the end // pose follows `actions.finish` setPlaybackRate(rate: number): void; // 1 normal, 2 double, negative = reverse; 0 is rejected // with a warning — use pause() getDuration(): number | null; // ONE pass of the content window, ms — the slider // maximum nobody hard-codes (× iterations = the run) getCurrentTime(): number | null; // ms from the start of the whole run, every iteration // included; null before mount setCurrentTime(time: number): void; // seek, ms from the start; clamped to the run getCurrentProgress(): number | null; // the same position as 0–1 of the whole run — the read // twin of the `showAtProgress` prop setCurrentProgress(p: number): void; // seek, 0–1 of the whole run}Play using action triggers (playUsingActionTriggers)
Section titled “Play using action triggers (playUsingActionTriggers)”Example:
react/play-using-action-triggers—pnpm example:docs, then open#react/play-using-action-triggers.
The simplest mode: the component starts the animation the way the file says it should — on load, on hover, on click — once enough of it is on screen.
import { SvgAnimationPlayer } from '@pixodesk/svg-animator-react';import animation from './animation.json';
export function Intro() { return <SvgAnimationPlayer doc={animation} playUsingActionTriggers />;}playUsingActionTriggers alone uses the triggers saved in the document — their event lists and the
visibility gate (Triggers — WHEN it plays).
Pass an object instead of true and that is the whole actionTriggers block to use for this mount —
a replacement, not a merge: the lists you leave out are simply not listed, never the file’s
values. How each action looks stays in the file’s actions, which the actions prop adjusts —
see Playback overrides.
// plays while hovered; when the pointer leaves it finishes the current iteration, then rests<SvgAnimationPlayer doc={animation} playUsingActionTriggers={{ start: 'hoverIn', stop: 'hoverOut' }} actions={{ stop: { completeIteration: true } }} />
// a click toggles stopped → playing → paused → playing (the state rule picks the list)<SvgAnimationPlayer doc={animation} playUsingActionTriggers={{ start: 'click', pause: 'click' }} />
// a click starts it, the next click stops it<SvgAnimationPlayer doc={animation} playUsingActionTriggers={{ start: 'click', stop: 'click' }} />Controlled time (showAtProgress / showAtTime)
Section titled “Controlled time (showAtProgress / showAtTime)”Example:
react/controlled-time—pnpm example:docs, then open#react/controlled-time.
Render one frame, and move through the animation by changing the prop. The player is not recreated on change — it just jumps to the new position.
import { useRef, useState } from 'react';import { SvgAnimationPlayer, type PxAnimationPlayerHandle } from '@pixodesk/svg-animator-react';import animation from './animation.json';
export function Scrubber() { const playerRef = useRef<PxAnimationPlayerHandle>(null); const [time, setTime] = useState(0); const [max, setMax] = useState(0); return ( <> <SvgAnimationPlayer doc={animation} showAtTime={time} playerRef={playerRef} onReady={() => setMax(playerRef.current?.getDuration() ?? 0)} /> <input type="range" min={0} max={max} value={time} onChange={e => setTime(+e.target.value)} /> </> );}showAtProgress is a position in the whole timeline (duration × iterations), from 0, the
first frame, to 1, the last — of one iteration when iterations is 'infinite';
showAtTime is a time in milliseconds from the start. Their read twins on the handle are
getCurrentProgress() and getCurrentTime().
Declarative playback — play, pause
Section titled “Declarative playback — play, pause”Example:
react/declarative—pnpm example:docs, then open#react/declarative.
play names a state, not a command — like a CSS class. true calls the built-in start,
false the built-in stop, and HOW each one looks is the file’s actions block — the start,
stop and finish behaviors and the stop’s completeIteration — which the actions prop adjusts
and which the triggers and the handle honour too
(Actions — how start, stop and finish look).
play={false} performs the stop action (actions.stop: reset or rewind) — it does not pause;
freeze with pause. With the defaults:
- One-shot document (a single pass):
play={true}plays to the end and holds there (startcontinue, finishhold);play={false}snaps back to the start (stopreset). A change moves from the current frame — interruptions turn around smoothly, never jump — and a mount never animates: the component renders the pose the props name (so a re-rendered list of icons does not all play at once). - Looping document (
iterations> 1 or'infinite'):play={true}runs,play={false}snaps to rest — or, withactions={{ stop: { completeIteration: true } }}, first finishes the current iteration (a heartbeat should not die mid-beat) and then rests. pausefreezes the current frame, immediately, and lifting it resumes. It only means something whileplayis true —play={false}forcibly unfreezes;pausewithoutplaywarns (PX1502) and changes nothing.
The everyday recipes — the same actions, the same settings:
// Hover icon (trash-can lid): plays to the end on enter, plays BACK on leave, from wherever it is<SvgAnimationPlayer doc={lid} play={hovered} actions={{ stop: 'rewind' }} />
// Splash / stamp — zero config: plays forward, off SNAPS back to clean.// To replay on EVERY click, let the start restart instead of continuing, and re-arm on finish:<SvgAnimationPlayer doc={splash} play={splashing} actions={{ start: 'restart' }} onFinish={() => setSplashing(false)} />
// Overlay art: appears instantly, retreats animated<SvgAnimationPlayer doc={art} play={open} actions={{ start: 'jumpToFinished', stop: 'rewind' }} />
// Spinner: runs while busy; when busy ends it finishes the current turn, then rests<SvgAnimationPlayer doc={spinner} play={busy} actions={{ stop: { completeIteration: true } }} />
// Heartbeat with two buttons: Pause freezes it (the freeze axis), Stop lets the beat finish first<SvgAnimationPlayer doc={beat} play={running} pause={paused} actions={{ stop: { completeIteration: true } }} />
// Doorbell — click plays once, start to finish; there is no "off", so it is a ref verb:<SvgAnimationPlayer doc={bell} playerRef={playerRef} /> /* + onClick={() => playerRef.current?.playOnce()} */The same two switches drive a pre-rendered SVG+CSS export as class names on its root —
px-anim-enabled (started; removing it resets) and px-anim-playing (running) — and the Vue
and React Native components share this component’s rule (resolveDeclarativePlayback in core,
acting through applyDeclarativeAction), so the behaviour cannot drift.
With none of playUsingActionTriggers / showAtProgress / showAtTime / play / pause set,
the component renders the first frame statically — playerRef on its own is such a case, so
playback waits for your call (playOnce() for the one-shot ping). A WRITE call on the ref while
play / pause or showAtProgress / showAtTime own playback warns
(PX1503) and is applied; the next prop change reasserts the props.
Playback overrides
Section titled “Playback overrides”Example:
playback/override-react—pnpm example:docs, then open#playback/override-react.
The same document can play differently in each place you mount it. Three props, the same on every player (Overriding from a player), and the document you passed is never modified:
timelineOptions— corrections to the file’sanimator.timelineblock, minus itsactionTriggersandactions: an object shaped like the file —iterations,delay,playDuration, the gate numbers; whoever needs more edits the document.actions— corrections to the file’stimeline.actions:{ stop: 'rewind' }changes how a stop looks and leaves every other setting as the file has it.playUsingActionTriggers—trueplays on the file’s triggers; an object is the wholeactionTriggersblock to use instead (Play using action triggers). To have your code drive instead, pass no control prop at all — the file’s triggers are then switched off andplayerRefis the driver.
Merge or replace — the one rule for the three:
timelineOptions, actions | patches — objects merge key by key, values replace, null deletes |
a playUsingActionTriggers object | replaces the file’s actionTriggers as a whole — its lists belong together |
// The file loops twice and plays on load; here it loops forever and waits for play() —// without `playUsingActionTriggers` the file's triggers are switched off, so only your code starts it.<SvgAnimationPlayer doc={animation} timelineOptions={{ iterations: 'infinite' }} playerRef={playerRef} />
// The file's triggers, with the timing corrected<SvgAnimationPlayer doc={animation} playUsingActionTriggers timelineOptions={{ outTime: 2000, playDuration: 1000 }} />
// These triggers instead of the file's, and a smooth retreat on hover-out<SvgAnimationPlayer doc={animation} playUsingActionTriggers={{ start: 'hoverIn', stop: 'hoverOut' }} actions={{ stop: 'rewind' }} />null deletes a key, so the default its absence means comes back:
<SvgAnimationPlayer doc={animation} playUsingActionTriggers timelineOptions={{ delay: null }} />actionTriggers and actions are not keys of timelineOptions — only playUsingActionTriggers carries triggers
and only actions the action settings, so every key is reachable exactly one way. The out-of-view
behavior goes in actions (actions={{ outOfView: 'continue' }}); the gate
numbers inViewThreshold and inViewDebounce are timeline keys and go in timelineOptions
(the visibility gate).
To repeat the whole animation, pass timelineOptions={{ iterations: 'infinite' }} — there is no
loop prop. A loop inside a file means something narrower: it repeats part of one property’s
keyframes (per-property loops).
Full merge rules — including what happens when the override changes the kind of timeline — are in Playback & triggers → How the merge works.
Only doc is required; everything else overrides what the document already says. React renders
the SVG; the player drives its attributes.
const SvgAnimationPlayer: FC<SvgAnimationPlayerProps>;
interface SvgAnimationPlayerProps { doc: PxDocumentInput; // the animation document (a .json import passes as-is); no URL form className?: string; // added to the root <svg> style?: CSSProperties; // set on the root <svg>
// Playback override — shaped like the file's `timeline` block, deep-merged over it. `null` at // a slot DELETES that key — see Playback overrides above: // timelineOptions={{ iterations, delay, iterationDirection, playDuration, inViewThreshold, … }} timelineOptions?: PxTimelinePatch; // the timing — everything but `actionTriggers` and `actions` actions?: PxActionsPatch; // how start, stop, finish and outOfView look: // actions={{ stop: { behavior: 'rewind', completeIteration: true } }}
// Control — the HIGHEST-priority one that is set picks the mode (Control modes, above) playerRef?: React.RefObject<PxAnimationPlayerHandle | null>; // never a mode: filled in every mode playUsingActionTriggers?: boolean | PxActionTriggersInput; // true: the document's own triggers; an object: these triggers // instead — { start, stop, pause } (`PxActionTriggersInput`), each one // event or a list of the events that list may name ('load' | // 'hoverIn' | 'hoverOut' | 'click'); resume is derived from them showAtProgress?: number; // controlled: 0–1 of duration × iterations // (one iteration when iterations is 'infinite') showAtTime?: number; // controlled: ms from the start play?: boolean; // the STATE: true calls `start`, false calls `stop` — how each // looks is the file's `actions` (and the `actions` prop); // a mount never animates pause?: boolean; // freeze the current frame NOW / resume; only while // `play` is true — play={false} forcibly unfreezes
// Lifecycle — no arguments onReady?: () => void; // the player exists and the SVG is rendered — fires once, async; // the push twin of isReady() onPlay?: () => void; // started, or resumed after a pause onPause?: () => void; // paused: the `pause` prop, the API's pause(), a `pause` trigger, or the gate onReset?: () => void; // back at the start pose — reset(), a stop or finish whose preset is `reset`, // the end of a rewind, the gate's `outOfView: reset` onFinish?: () => void; // reached the end — every iteration played, or finish() was called; // not when stopped early onDestroy?: () => void; // the player was destroyed — unmount, or new `doc` content re-created it onStop?: () => void; // the built-in `stop` action was performed — a `stop` trigger, play={false}, // stop(); now, or after the iteration it waited for. A pause is // onPause, the end onFinish, a removal onDestroy — never this one
// Diagnostics — the shared channel (the API at a glance): onWarn = it plays, but something was // ignored, degraded or misspelled; onError = this instance will not play, `playerRef` stays empty onWarn?: (d: PxDiagnostic) => void; onError?: (d: PxDiagnostic) => void; suppressWarnings?: boolean; suppressErrors?: boolean;}Each diagnostic is { code, kind, data?, message, error? }. code is a number you can switch on — its description is on the codes page, which message links to; data carries the specifics. kind says who can act on it:
document (repair the file) · host (fix the page) · platform (the browser could not do it;
the player degraded) · usage (fix the props you passed) · internal (report it to us). So you
can route rather than just log — surface document problems in a build check, for instance. Once
you know what a document has to say and tolerate it, suppressWarnings keeps it out of the console.
Passing a different doc (or changing className / style / the control mode) throws the
old player away and builds a new one; the old instance emits onReset and onDestroy on its
way out. Changing showAtProgress / showAtTime does not recreate anything.
CSS-flavor SVGs — SvgCssAnimationPlayer
Section titled “CSS-flavor SVGs — SvgCssAnimationPlayer”Example:
react/css-svgr—pnpm example:docs, then open#react/css-svgr.
For a pre-rendered SVG + CSS animation file imported as a component with
SVGR (@svgr/webpack, vite-plugin-svgr), this small wrapper adds
the triggers and the visibility gate. It renders a <div> of its own around your SVG component —
that is what SvgCssAnimationPlayer becomes on the page — and starts, pauses or resets the
animation by switching the file’s CSS classes on that <div>:
import { SvgCssAnimationPlayer } from '@pixodesk/svg-animator-react';import AnimationSvg from './animation.svg?react'; // vite-plugin-svgr
export function HoverLogo() { return ( <SvgCssAnimationPlayer actionTriggers={{ start: 'hoverIn', pause: 'hoverOut' }} style={{ width: 400, height: 400 }}> <AnimationSvg /> </SvgCssAnimationPlayer> );}actionTriggers is the JSON player’s block — the same lists over the same events, read by
the same rules (Triggers — WHEN it plays) —
and the gate is the same gate. Absent actionTriggers play on load; an empty block lists nothing.
// Wraps the SVG in a div and drives it by toggling class names — `px-anim-enabled` once// started (paused), plus `px-anim-playing` while running.const SvgCssAnimationPlayer: FC<{ children: ReactNode; // the SVGR-imported SVG component actionTriggers?: PxActionTriggersInput; // the requests: { start, stop, pause }, each one event or a // list of the events that list may name ('load' | 'hoverIn' | // 'hoverOut' | 'click'); resume is derived from them actions?: Pick<PxActions, 'outOfView'>; // only `outOfView` applies: { outOfView: 'pause' } or // { outOfView: { behavior } } — 'pause' (default) | 'continue' | // 'reset' while nobody can see it inViewThreshold?: number; // 0–1 of the SVG on screen before it may run; default 0.5; 0 = any visible pixel inViewDebounce?: number; // ms it must hold first; default 150 className?: string; // on the wrapper div style?: CSSProperties; // on the wrapper div}>;Accepted limits of a class toggle — how the moves look stays in the file’s CSS, so here:
- the start preset acts as
continue— a class cannot restart keyframes or jump to their end; - the stop preset acts as
reset— a class cannot play keyframes backwards; - the stop’s
completeIteration,showWhenStoppedand the finish preset are the file’s own CSS and are not changed from here.
⚠️ Don’t put the SAME SVG file on a page twice. You can have as many
<SvgCssAnimationPlayer>on a page as you like, each with a different file. What does not work is the same file twice: the imported component is the file’s markup, element ids included, so two copies share the same ids and their masks and gradients cross over. To show one animation several times, use the JSON component instead — the player gives every copy its own ids (read more).
SVGR strips <script> tags, so only the pure CSS flavor works this way. Files with scripts
(JS triggers / JS animation) should be inlined as raw HTML, or switched to JSON.
API reference
Section titled “API reference”Everything is spelled out above: SvgAnimationPlayer and its props under Props, the
handle under Imperative API, SvgCssAnimationPlayer under
CSS-flavor SVGs. The callbacks and diagnostics are
the shape every player shares — the API at a glance.
Also exported: ● SvgAnimationPlayerProps, PxAnimationPlayerHandle,
SvgAnimationPlayerCallbacks (the on* props as a standalone type), and PxDocument
(the document type, for casting a JSON import — TypeScript).