Skip to content

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.

Terminal window
npm install @pixodesk/svg-animator-react
import { 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).

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).

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().

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 (start continue, finish hold); play={false} snaps back to the start (stop reset). 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, with actions={{ stop: { completeIteration: true } }}, first finishes the current iteration (a heartbeat should not die mid-beat) and then rests.
  • pause freezes the current frame, immediately, and lifting it resumes. It only means something while play is true — play={false} forcibly unfreezes; pause without play warns (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.

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’s animator.timeline block, minus its actionTriggers and actions: an object shaped like the file — iterations, delay, playDuration, the gate numbers; whoever needs more edits the document.
  • actions — corrections to the file’s timeline.actions: { stop: 'rewind' } changes how a stop looks and leaves every other setting as the file has it.
  • playUsingActionTriggers — true plays on the file’s triggers; an object is the whole actionTriggers block 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 and playerRef is the driver.

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
// 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.

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, showWhenStopped and 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.

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).