Player reference
svgmo-player.js plays svgmo files (.json) in a web page. It adds:
- the
<svg-mo>element, and - an
Svgmoobject for use from scripts.
One file, about 30 KB (11 KB gzipped), no dependencies. See Getting started for installation.
A page with every example below running is served by the app at /player-demo.html.
The <svg-mo> element
<script src="svgmo-player.js"></script>
<svg-mo src="logo.json" autoplay loop></svg-mo>
Attributes
| Attribute | Values | Default | Meaning |
|---|---|---|---|
src |
URL | none | The svgmo file to load. Changing it loads the new file. |
autoplay |
present / "false" |
off | Start playing once loaded. Only applies when trigger is not set. |
loop |
present / "false" |
on | Repeat forever. loop="false" plays once and stops on the last frame. |
speed |
number above 0 | 1 |
Playback rate. 2 is twice as fast, 0.5 is half speed. |
direction |
"reverse" or "-1" |
forward | Play backwards. |
trigger |
"hover", "click", "visible" |
none | What starts playback. See below. |
respect-reduced-motion |
present | off | If the visitor's system asks for reduced motion, show the last frame and do not animate. |
aria-label |
text | none | Description for screen readers. The drawing is exposed as an image. |
Triggers
trigger |
Behaviour |
|---|---|
hover |
Plays while the pointer is over the element. On leave, stops and returns to the first frame. |
click |
Each click toggles play and pause. |
visible |
Starts when the element scrolls into view. |
With a trigger set, autoplay is ignored.
Size
- With no size from the page, the element is as wide as the animation's artboard, in pixels, and never wider than its container.
- It always keeps the artboard's proportions.
- Give it a width (
style="width: 300px",width: 100%, a CSS class) and the height follows.
Off-screen behaviour
The element pauses itself when it is scrolled out of view and resumes when it is visible again. This saves CPU and battery and needs no setup.
Inline data instead of a file
To avoid a second request, put the animation inside the tag:
<svg-mo autoplay>
<script type="application/json">
{ "format": "svgmo", "v": 1, "meta": { "generator": "svgmo" }, "comp": { ... } }
</script>
</svg-mo>
src wins when both are present.
Methods
const el = document.querySelector("svg-mo");
el.play(); // start, or resume
el.pause();
el.stop(); // pause and return to the first frame
el.seek(30); // jump to frame 30
el.player; // the player behind the element (below)
Events
The element fires DOM events:
| Event | When | event.detail |
|---|---|---|
ready |
The file is loaded and the first frame is drawn | |
play |
Playback starts | |
pause |
Playback stops, for any reason | |
loop |
A loop restarts | |
complete |
A non-looping animation reaches its end | |
error |
The file could not be loaded or is not valid | message text |
el.addEventListener("complete", () => console.log("done"));
el.addEventListener("error", (e) => console.warn(e.detail));
Per-frame updates are not sent as DOM events. Use el.player.on("frame", fn).
The script API
For cases where a tag is not enough.
Svgmo.load(options)
Loads an animation into an element and returns the player.
const player = await Svgmo.load({
container: "#stage", // element, or a CSS selector
src: "logo.json", // URL of the file
autoplay: true,
loop: false,
});
| Option | Type | Default | Meaning |
|---|---|---|---|
container |
element or selector | required | Where to draw |
src |
string | URL of an svgmo file | |
data |
string or object | The animation itself, instead of src |
|
autoplay |
boolean | false |
Play once loaded |
loop |
boolean | true |
Repeat |
speed |
number | 1 |
Playback rate |
direction |
1 or -1 |
1 |
Forward or backward |
Give src or data. The promise resolves after loading. If the file fails to load the promise still resolves; listen for error or check player.isLoaded.
Unlike the element, a player created this way does not size its container, does not pause off-screen and has no triggers. The drawing fills the container's width and height, keeping proportions.
new Svgmo.Player(container, options)
The same player without loading anything yet. Call player.load(source) yourself.
Player methods
| Method | Meaning |
|---|---|
load(source) |
Load a URL, JSON text or a parsed object. Returns a promise of true or false. |
play() |
Start or resume. From the end, starts over. |
pause() |
Stop where it is. |
stop() |
Pause and return to the first frame. |
toggle() |
Play if paused, pause if playing. |
seek(frame) |
Jump to a frame. Fractions are allowed. |
seekTime(seconds) |
Jump to a time. |
seekProgress(p) |
Jump to a position from 0 (start) to 1 (end). |
setSpeed(n) |
Change the rate. |
setLoop(bool) |
Turn looping on or off. |
setDirection(1 | -1) |
Forward or backward. |
on(event, fn) |
Listen. Returns a function that removes the listener. |
destroy() |
Stop, remove the drawing and all listeners. |
Player properties
| Property | Meaning |
|---|---|
frame |
Current frame (fractional while playing) |
totalFrames |
Length in frames |
fps |
Frames per second of the file |
duration |
Length in seconds |
isPlaying |
true while playing |
isLoaded |
true once a file is loaded |
size |
{ width, height } of the artboard, or null |
lastWrites |
Attributes changed by the last drawn frame (see Performance) |
Player events
ready, play, pause, loop, complete, error, and frame (called on every drawn frame with the frame number).
const off = player.on("frame", (frame) => {
progressBar.value = frame / player.totalFrames;
});
// later
off();
Recipes
Scroll-driven animation
<svg-mo id="story" src="story.json" style="width: 100%"></svg-mo>
<script>
const el = document.getElementById("story");
addEventListener("scroll", () => {
const max = document.documentElement.scrollHeight - innerHeight;
el.player.seekProgress(max > 0 ? scrollY / max : 0);
}, { passive: true });
</script>
Play once, then hold, then replay on click
<svg-mo id="tick" src="tick.json" autoplay loop="false"></svg-mo>
<script>
const tick = document.getElementById("tick");
tick.addEventListener("click", () => { tick.stop(); tick.play(); });
</script>
A button whose icon animates on hover
<button class="like">
<svg-mo src="heart.json" trigger="hover" loop="false" style="width: 24px"></svg-mo>
Like
</button>
Scrubber and transport buttons
const el = document.querySelector("svg-mo");
play.onclick = () => el.play();
pause.onclick = () => el.pause();
scrub.oninput = () => { el.pause(); el.player.seekProgress(Number(scrub.value)); };
el.player.on("frame", (f) => { scrub.value = f / el.player.totalFrames; });
React
Custom elements work in JSX. Load the script once, then:
export function Logo() {
return <svg-mo src="/logo.json" autoplay loop style={{ width: 200 }} />;
}
For methods, use a ref: ref.current.play(). In TypeScript, declare the tag in JSX.IntrinsicElements to avoid a type error.
Swap the animation
el.setAttribute("src", "other.json"); // element
await player.load("other.json"); // script API
A player that was playing keeps playing the new file.
Accessibility
- Add
aria-labelto describe the animation, oraria-hidden="true"if it is decoration. - Add
respect-reduced-motionso visitors who ask for less motion get a still picture. - With
trigger="click", wrap the element in a<button>so it is reachable by keyboard.
Performance
The player builds the drawing once and then, on each frame, changes only what is different.
- Still layers cost nothing. A layer with no animation is drawn once and never touched again, however many points it has.
- Moving layers update only what moves. A shape that slides or spins gets a new
transform; its outline is not rewritten. A colour change rewrites only the colour. - A layer that follows an animated parent is treated as moving.
- Shape morphing (a path with path keyframes) does rewrite that one outline every frame; that is the costly case. Keep morphing shapes to a sensible point count.
- Off-screen
<svg-mo>elements pause on their own. - Playback follows the display refresh rate; the file's frame rate sets the speed, not how often it draws.
- If a browser tab was in the background, playback continues from where it was rather than jumping ahead.
Measured in Chromium, script and DOM time per frame (painting not included):
| Scene | Rebuild everything each frame | This player |
|---|---|---|
| 300 still shapes of 120 points each (36,000 points) plus 6 moving layers | about 22 ms | about 0.04 ms, 6 attribute writes |
| The small sample scene (3 animated layers) | about 0.04 ms | about 0.02 ms |
At 60 frames a second a frame has about 16 ms, so the first row is the difference between dropping frames and having time to spare.
player.lastWrites tells you how many attributes the last frame changed; 0 means nothing on screen changed. It is useful when checking why an animation is heavy.
Loading and hosting
srcis fetched withfetch, so the file must be reachable over HTTP(S) and allowed by CORS when it is on another domain.- Opening a page from disk (
file://) blocks the request in most browsers. Use a local web server, or inline the data. - Any JSON or text content type works. Servers already send
.jsonasapplication/json.
Errors
A failed load fires error with a message and leaves the element empty. Messages include:
| Message | Meaning |
|---|---|
Could not load <url> (404) |
The file was not found, or the server refused |
The file is not valid JSON. |
The response is not JSON, often an HTML error page |
This is a Lottie file, not an svgmo file. |
Export svgmo JSON from the editor instead |
This file uses format version N; this reader understands up to 1. Update the player. |
The file is newer than the player |
Other text such as layers must be an array |
The file is damaged or hand-edited incorrectly |
Earlier names
<vector-motion> and VectorMotion.load() were the first names of the tag and the script object. Both still work and behave the same as <svg-mo> and Svgmo.load(). The player also plays files saved with the old .svgmo and .vmotion extensions.
Browser support
Uses custom elements, shadow DOM, IntersectionObserver, fetch and inline SVG: all current browsers. Tested in Chromium; Safari and Firefox have not been run.