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 — autoplay, declarative props, an imperative ref, or controlled time.

Terminal window
npm install @pixodesk/svg-animator-react
import { PixodeskSvgAnimator } from '@pixodesk/svg-animator-react';
import animation from './animation.json';
export function Logo() {
return <PixodeskSvgAnimator doc={animation} autoplay />;
}

The component renders the root <svg> of the document; size it with className / style or by sizing the parent (the SVG keeps its viewBox).

Three control modes, plus a handle that is not one. Set more than one control prop and the highest-priority one wins — progress / time → play / pause → autoplay — and the component warns, naming both props and the winner. apiRef is filled in every mode and never changes which one you are in, so you can always call play() / pause() yourself; apiRef 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.

Example: react/imperative — pnpm example:docs, then open #react/imperative.

Pass a ref as apiRef. Once the component has mounted, the ref holds the playback API, so any button, timer or effect in your app can start, pause, or jump to any point in the animation.

import { useRef } from 'react';
import { PixodeskSvgAnimator, type ReactAnimatorApi } from '@pixodesk/svg-animator-react';
import animation from './animation.json';
export function Player() {
const api = useRef<ReactAnimatorApi>(null);
return (
<>
<PixodeskSvgAnimator doc={animation} apiRef={api} />
<button onClick={() => api.current?.play()}>Play</button>
<button onClick={() => api.current?.pause()}>Pause</button>
<button onClick={() => api.current?.setPlaybackRate(-1)}>Reverse</button>
</>
);
}

What apiRef.current gives you — the web API minus destroy / getRootElement / isReady, because the component owns the element’s lifetime. It is core’s PxAnimatorHandle under this package’s name; VueAnimatorApi and RnAnimatorApi are the same type, so the three cannot drift:

interface ReactAnimatorApi {
isPlaying(): boolean; // true while running; false when paused, finished or not started
play(): void; // start, or resume from the current time
pause(): void; // hold the current frame
cancel(): void; // stop and reset to the start
finish(): void; // jump to the end and hold it
setPlaybackRate(rate: number): void; // 1 normal, 2 double, negative = reverse; 0 is rejected
// with a warning — use pause()
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 `progress` prop
setCurrentProgress(p: number): void; // seek, 0–1 of the whole run
}

Example: react/autoplay — pnpm example:docs, then open #react/autoplay.

The simplest mode: the component starts the animation the way the file says it should — on load, on hover, on click, or when scrolled into view.

import { PixodeskSvgAnimator } from '@pixodesk/svg-animator-react';
import animation from './animation.json';
export function Intro() {
return <PixodeskSvgAnimator doc={animation} autoplay />;
}

Uses the trigger saved in the document — on load, on hover, on click, when scrolled into view — and its out action. Override it for this one mount with the start shortcut, or with timeline={{ trigger: { … } }} for the rest of the trigger — see Playback overrides.

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 animator is not recreated on change — it just jumps to the new time.

import { useState } from 'react';
import { PixodeskSvgAnimator } from '@pixodesk/svg-animator-react';
import animation from './animation.json';
export function Scrubber() {
const [time, setTime] = useState(0);
return (
<>
<PixodeskSvgAnimator doc={animation} time={time} />
<input type="range" min={0} max={2000} value={time} onChange={e => setTime(+e.target.value)} />
</>
);
}

progress is a position in the whole timeline (duration × iterations), from 0, the first frame, to 1, the last; time is a time in milliseconds from the start.

Example: react/declarative — pnpm example:docs, then open #react/declarative.

Drive playback from your own state with two booleans — handy when play/pause is already part of your component’s state (a toggle, a visibility flag) and you would rather not hold a ref.

import { useState } from 'react';
import { PixodeskSvgAnimator } from '@pixodesk/svg-animator-react';
import animation from './animation.json';
export function Controlled() {
const [play, setPlay] = useState(false);
const [pause, setPause] = useState(false);
return (
<>
<PixodeskSvgAnimator doc={animation} play={play} pause={pause} />
<button onClick={() => { setPlay(true); setPause(false); }}>Play</button>
<button onClick={() => setPause(true)}>Pause</button>
</>
);
}

play && !pause plays; pause pauses; play === false holds where it is (it used to jump to the end); a pause that is switched back off resumes.

With none of autoplay / progress / time / play / pause set, the component renders the first frame statically — apiRef on its own is such a case, so playback waits for your play().

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

The same document can play differently in each place you mount it. timeline takes an object shaped exactly like the file’s own animator block and deep-merges it over what the file says — the document you passed is never modified.

// The file loops twice and starts on load; here it loops forever and waits for play().
<PixodeskSvgAnimator
doc={animation}
timeline={{ iterations: 'infinite', trigger: { start: 'none' } }}
apiRef={apiRef}
/>

Objects merge key by key, values replace, and null deletes a key so the default its absence means comes back:

<PixodeskSvgAnimator doc={animation} autoplay timeline={{ delay: null }} />

duration, delay, iterations and start are also plain props, because duration={2000} reads better than a nested object; a prop wins over the same key inside timeline. To ignore the file’s playback settings entirely and start from the player’s defaults, add resetTimeline.

Full merge rules — including what happens when the override changes the kind of timeline — are in Playback & triggers → Overriding from a player.

Only doc is required; everything else overrides what the document already says. React renders the SVG; the player drives its attributes.

const PixodeskSvgAnimator: FC<PixodeskSvgAnimatorProps>;
interface PixodeskSvgAnimatorProps {
doc: PxAnimatedSvgDocument; // the animation document (the JSON format page); no URL form
className?: string; // added to the root <svg>
style?: CSSProperties; // set on the root <svg>
// Playback override — one object, shaped exactly like the file's `timeline` block,
// deep-merged over it. `null` at a slot DELETES that key — see Playback overrides above:
// timeline={{ engine, frameRate, fillMode, direction,
// trigger: { mouseOut, finish, visibilityThreshold } }}
timeline?: PxTimelinePatch | string; // a JSON string is accepted too
resetTimeline?: boolean; // start from the player's defaults, `timeline` on top
// Shortcuts — a shortcut wins over the same key inside `timeline`
duration?: number; // ▸ timeline.duration (one iteration, ms)
delay?: number; // ▸ timeline.delay (ms). Negative skips ahead: -500 starts
// at once from the frame at 0.5 s
iterations?: number | 'infinite'; // ▸ timeline.iterations; 'infinite' never stops
start?: PxTriggerStart; // ▸ timeline.trigger.start: 'load' | 'mouseOver' | 'click' |
// 'scrollIntoView' | 'none' (only a play() from code)
// Control — the HIGHEST-priority one that is set picks the mode (Control modes, above)
apiRef?: React.RefObject<ReactAnimatorApi | null>; // never a mode: filled in every mode
autoplay?: boolean; // obey the document's own trigger — the Start setting from the editor
progress?: number; // controlled: 0–1 of duration × iterations
// (one iteration when iterations is 'infinite')
time?: number; // controlled: ms from the start
play?: boolean; // true: play regardless of the trigger; false: hold where it is
pause?: boolean; // hold the current frame; false again resumes
// Lifecycle — no arguments
onPlay?: () => void; // started, or resumed after a pause
onPause?: () => void; // paused: the `pause` prop, the API's pause(), or a trigger's out action
onCancel?: () => void; // stopped and back at the start state
onFinish?: () => void; // reached the end — every iteration played, or finish() was called;
// not when stopped early
onRemove?: () => void; // the player was destroyed — unmount, or a new `doc` re-created it
onStop?: () => void; // after any of onPause / onCancel / onFinish / onRemove — the one to use
// when you only care that it is no longer playing
// 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, `apiRef` stays empty
onWarn?: (d: PxDiagnostic) => void;
onError?: (d: PxDiagnostic) => void;
muteWarn?: boolean; muteError?: 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, muteWarn keeps it out of the console.

Passing a different doc (or changing className / style / the control mode) throws the old animator away and builds a new one; the old instance emits onCancel, onRemove and onStop on its way out. Changing progress / time does not recreate anything.

CSS-flavor SVGs — PixodeskSvgCssAnimator

Section titled “CSS-flavor SVGs — PixodeskSvgCssAnimator”

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 hover / click / scroll triggers. It renders a <div> of its own around your SVG component — that is what PixodeskSvgCssAnimator becomes on the page — and starts, pauses or resets the animation by switching the file’s CSS classes on that <div>:

import { PixodeskSvgCssAnimator } from '@pixodesk/svg-animator-react';
import AnimationSvg from './animation.svg?react'; // vite-plugin-svgr
export function HoverLogo() {
return (
<PixodeskSvgCssAnimator start="mouseOver" mouseOut="pause" style={{ width: 400, height: 400 }}>
<AnimationSvg />
</PixodeskSvgCssAnimator>
);
}
// Wraps the SVG in a div and drives it by toggling class names — `px-anim-enabled` once
// started, plus `px-anim-playing` while running.
const PixodeskSvgCssAnimator: FC<{
children: ReactNode; // the SVGR-imported SVG component
start?: PxTriggerStart; // 'load' (default) | 'mouseOver' | 'click'
// — 'none' does nothing here (no play())
offScreen?: PxOffScreenAction; // 'pause' (default) | 'continue' | 'reset' — what happens
// while nobody can see it, whatever started it
mouseOut?: PxMouseOutAction; // 'continue' (default) | 'pause' | 'reset'
// — 'reverse' is accepted but acts as 'continue': a class
// toggle cannot run CSS keyframes backwards
visibilityThreshold?: number; // 0–1 of the SVG on screen before it may run; default 0.5
visibilityDebounce?: number; // ms it must hold first; default 150
className?: string; // on the wrapper div
style?: CSSProperties; // on the wrapper div
}>;

⚠️ Don’t put the same SVG file on a page twice. You can have as many <PixodeskSvgCssAnimator> 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.

The component renders real SVG markup on the server and starts the animator in an effect on the client, so it works in the App Router — mark the file that uses it as a client component:

'use client';
import { PixodeskSvgAnimator } from '@pixodesk/svg-animator-react';
import animation from './animation.json';
export default function Hero() {
return <PixodeskSvgAnimator doc={animation} autoplay />;
}

JSON imports work out of the box in Next.js; for a CSS-flavor SVG use @svgr/webpack.

Everything is spelled out above: PixodeskSvgAnimator and its props under Props, the handle under Imperative API, PixodeskSvgCssAnimator under CSS-flavor SVGs. The callbacks and diagnostics are the shape every player shares — the API at a glance.

Also exported: ● PixodeskSvgAnimatorProps, ReactAnimatorApi, PixodeskSvgAnimatorCallbacks (the six on* props as a standalone type).