Skip to content

Pre-rendered SVG on the web

Put a pre-rendered SVG on a page by inlining it — paste the <svg> into the HTML, or let your framework or static-site generator inline the file. It is a normal .svg with the animation already inside, and once its markup is part of the page, everything in it works: the animation plays, the triggers respond, and its elements can be reached from CSS and JavaScript.

What does not work is treating it as a picture: <img src>, SVG <image>, CSS background-image. A browser runs no scripts inside an image and nothing on the page can reach into it, so a pre-rendered SVG used that way is a static frame.

This page shows the embedding options and how much control each flavour gives you once it is there. For which flavour to pick, see Choosing a format.

Example: prerendered/img-csspnpm example:docs, then open #prerendered/img-css.

Each of the three works with every flavour:

MethodNotes
Inline — paste the <svg>…</svg> into the HTMLthe animation becomes part of your page, like any other HTML: your CSS can style its elements, your JavaScript can reach them (for example to add the play / pause classes), and the file’s own script — if its flavour has one — runs as a normal page script. The most capable option
Build-time inline — the framework or static-site generator inlines the fileyou keep a.svg as a separate file in your project, and the tool copies its contents into the HTML when the site is built. The page ends up exactly as in the row above — as if you had pasted the <svg> in by hand — but the animation stays editable as its own file. How to set this up per tool: Static sites & CMS
<object data="a.svg"> / <iframe src="a.svg">runs in its own document; scripts work but cannot be reached from the page. Not recommended

Not as a picture. <img src="a.svg">, SVG <image> and CSS background-image show a static frame: no script runs inside an image, and the page cannot reach in to add the play classes. Inline the file instead.

⚠️ Inline each .svg file only once per page. Every element in a pre-rendered SVG has an id (id="_px_…"), and its masks, gradients, clip paths and JS bindings refer to those ids. If you inline the same file twice, the same ids appear twice on the page — the second copy’s mask or gradient then resolves to the first copy’s, and things break in ways that are hard to spot.

Need the same animation several times on one page? Either export a separate file for each place — every export from the editor gets its own element ids, so the copies don’t clash — or use the JSON format, where the player gives every instance fresh ids for you, so any number of copies coexist.

Pick it whenWhat you getWhat you give up
1 · SVG + CSS animationa loop, an icon, decoration — it just needs to play, on load or on hover• the smallest file
• no JavaScript at all
• a hover trigger through CSS :hover
• imports as a component through SVGR / vite-svg-loader
• only what CSS @keyframes can express — path morphing only in recent browsers, no gradient geometry, filters or text on a path
• no click or scroll trigger
• playback is controlled with CSS classes only
2 · SVG + CSS animation + JS triggersit should start on click or scroll into view, or needs an out action or reset-on-finish, and you don’t want to write codethe same file plus a few inline lines of script (added by the editor app) — no library — that wire the trigger, the out action and reset-on-finish for you• the same CSS limits
• no jumping to a time, no reverse, no speed change
• the <script> rules out SVGR import — inline it
3 · SVG + JS animationthe animation uses something CSS cannot do, or you want the full playback API• every animation type
• play, pause, jump to any point, reverse, change speed
• self-contained
• the player is embedded in the file (25–38 KB; the editor can instead leave it out and let the page load the player library, @pixodesk/svg-animator-web, separately — see Flavour 3)
• the <script> rules out SVGR import
• out of the box, your own page scripts cannot control the playback: the script inside the file starts the player and keeps it to itself, in a local variable. Flavour 3 shows the workaround — and why JSON is the better choice when you need control from code

If you find yourself reaching for Flavour 3 and wanting to control it from code, the JSON format is usually the better answer: same player, no embedded copy per file, and any number of instances per page.

The full comparison — including JSON, and what each engine can animate — is in Choosing a format → Pros and cons and Engine pros and cons.

Example: prerendered/inline-csspnpm example:docs, then open #prerendered/inline-css.

In this flavour the file contains no JavaScript at all — the animation is written entirely in CSS, which every browser plays by itself. Inside the .svg there is a <style> block holding the movement as CSS @keyframes rules, and each animated element carries a class that connects it to its rules. Nothing needs to load or run for it to play.

The complete SVG + CSS animation export of a bouncing ball, start option On load — exactly as the editor writes it (this is the file the example inlines):

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" id="_px_1" class="px-anim-enabled px-anim-playing" data-px-meta="runtime:{useCssAnimation:true},animator:{duration:1000,mode:'auto',iterations:'infinite',direction:'alternate',trigger:{startOn:'load',outAction:'pause'}}">
<style>@keyframes _px_2 {0% {transform:translate(200px,60px);animation-timing-function:cubic-bezier(0.33,0,0.67,0.33);}
100% {transform:translate(200px,340px)}}
.px-anim-enabled ._px_3 { animation: 1000ms _px_2 infinite alternate both; }
.px-anim-enabled.px-anim-playing .px-anim-element {animation-play-state: running !important;}
.px-anim-enabled:not(.px-anim-playing) .px-anim-element {animation-play-state: paused;}</style>
<ellipse id="ball" class="px-anim-element _px_3" fill="#0087ff" transform="translate(200,60)" rx="40" ry="40" data-px-meta="animate:{transform:{keyframes:[{time:0,value:{translate:[200,60]},easing:[0.33,0,0.67,0.33]},{time:1000,value:{translate:[200,340]}}]}}"/>
</svg>

Three things to notice:

  • the @keyframes and the per-element class are the animation;
  • the two classes on the root <svg> are the play state;
  • data-px-meta is editor bookkeeping that lets the file be re-opened with its effects intact — browsers ignore it (Meta in pre-rendered SVG).

Whether the animation runs is decided by two CSS classes on the root <svg> element. The animation only plays while both are present; take one away and it stops. Since adding and removing classes is something any CSS rule or line of JavaScript can do, this is also how you control playback in this flavour:

Classes on the root <svg> (or a wrapper you control)State
(none)idle — the animation has not started
px-anim-enabledstarted but paused
px-anim-enabled px-anim-playingplaying

Exported with the On load start option, the file has px-anim-enabled px-anim-playing on its root already, so it plays the moment it is inlined — nothing has to happen at runtime. Exported with On mouse over, the root carries px-anim-enabled only, and a CSS :hover rule inside the file supplies the playing state while the pointer is over it — a hover trigger with no script at all. On click and When visible have no CSS equivalent; a pure-CSS export falls back to On load for them. So a pure-CSS file starts an animation by itself only in those two cases — on load and on hover. For anything else, the play classes have to be added to the <svg> at the right moment, and that can be done by one of three things: the small trigger script the editor puts into the file in flavour 2, the PixodeskSvgCssAnimator wrapper component (React / Vue — described just after the code), or a few lines of your own JavaScript:

const svg = document.getElementById('_px_1'); // the exported <svg>'s own id — see the file above
svg.classList.add('px-anim-enabled', 'px-anim-playing'); // play
svg.classList.remove('px-anim-playing'); // pause
svg.classList.remove('px-anim-enabled', 'px-anim-playing'); // reset to the start

This is exactly what the React and Vue PixodeskSvgCssAnimator components do for you — they wrap an SVGR / vite-svg-loader import in a <div> and toggle these classes on hover, click or scroll-into-view. See React → CSS-flavour SVGs and Vue → CSS-flavour SVGs.

Flavour 2 — SVG + CSS animation + JS triggers

Section titled “Flavour 2 — SVG + CSS animation + JS triggers”

Same file plus a small <script data-px-script="true"> — a few lines the editor writes, no library — that listens for the trigger you chose (mouse over, click, scroll into view) and toggles the classes above, including the out action (continue / pause / reset / reverse) and reset on finish. It is the Use JS Triggers switch in the editor’s trigger panel. Nothing to wire up: inline the file and it responds to the user.

Because it has a <script>, it cannot go through SVGR or vite-svg-loader, which strip scripts. Inline it, or use an <object>.

Example: prerendered/inline-jspnpm example:docs, then open #prerendered/inline-js.

In this flavour nothing in the file is animated by CSS. Instead, the elements are drawn as plain, static SVG, and the file carries a <script> holding the animation data — which elements move, and how, over time. When the page shows the file, that script hands the data to the web player, and the player animates the elements. Below is the editor’s SVG + JS animation export of the same bouncing-ball animation with the Embed JS Player option off — exactly as written (the example inlines it after loading the player library). With Embed JS Player switched on, the editor inlines the player into that same <script data-px-script="true">, so the file is self-contained.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" id="_px_1" data-px-meta="runtime:{externalJs:true},animator:{duration:1000,mode:'auto',iterations:'infinite',direction:'alternate',trigger:{startOn:'load',outAction:'pause'}}">
<ellipse id="ball" fill="#0087ff" transform="translate(200,60)" rx="40" ry="40" data-px-meta="animate:{transform:{keyframes:[{time:0,value:{translate:[200,60]},easing:[0.33,0,0.67,0.33]},{time:1000,value:{translate:[200,340]}}]}}"/>
<script data-px-script="true">
//<![CDATA[
(function() {
var a = PixodeskAnimator.createAnimator({"data":
{"id":"_px_1","type":"svg","animator":{"mode":"auto","timeline":{"type":"clock","duration":1000,"trigger":{"startOn":"load","outAction":"pause"},"iterations":"infinite","direction":"alternate"},"definitions":{"animations":{"a0":{"transform":{"keyframes":[{"time":0,"value":{"translate":[200,60]},"easing":[0.33,0,0.67,0.33]},{"time":1000,"value":{"translate":[200,340]}}]}}}},"animateById":{"#ball":["a0"]}}}
});
})();
//]]>
</script>
</svg>

The <script> does not repeat the drawing. A JSON animation file normally carries both the elements and their animation; here the elements are already in the file as ordinary SVG markup, so the script holds only the animation part — the JSON format’s animator block. Inside it, definitions.animations describes the movements, and animateById lists which element id gets which movement. When the file loads, the player finds each element in the SVG by its id and animates it in place.

Because a real player runs the animation, this flavour supports every animation type — nothing falls back, nothing is dropped. To keep the file small, the editor does not embed the full player: it picks a trimmed build that contains only what your chosen engine mode needs — choosing the “WAAPI” engine mode in the editor gives the smallest file, because the build for it carries no frame-loop engine.

The player does not have to live inside the file at all. Switch the editor’s Embed JS Player option off and the exported file contains only the drawing and the animation data; your page then loads the player library, @pixodesk/svg-animator-web, as a normal script (how to host it on your own site). Do this when several exported files sit on one page: they all use that one copy of the player instead of each file carrying its own.

One limitation to know. The script inside the file starts the player and gets back a full remote control for the animation — the same playback API the web player has, with play, pause and everything else. But the script keeps that remote control in a variable of its own, where scripts on your page cannot see it. So the animation plays fine, but your code has no way to pause it or jump around in it. If you need that control, you have two options:

  • choose the Manually from JS start option in the editor, then edit the exported script by hand so it puts the remote control somewhere your code can reach, for example window.myAnim = PixodeskAnimator.createAnimator(…);
  • or — simpler, and recommended — use the JSON format instead: controlling the animation from code is exactly what it is made for.

Setting the size of an inlined pre-rendered SVG works the same as for any inlined SVG: keep its viewBox attribute (it preserves the proportions), remove or override the width / height attributes, and give the size in CSS — on the <svg> itself or on the element that contains it, e.g. svg { width: 100%; height: auto }.