Scripting Animated SVGs
An animated Molkit export is one self-contained SVG file driven by one SMIL timeline. No JavaScript ships inside it, no runtime library is required, and the browser plays it on its own. But because SMIL runs on the standard SVG document timeline, you can script it from the host page: pause it, scrub it, or turn it into a click-to-advance stepped reveal. This page covers the file anatomy, the control API, and three copy-paste recipes.
If you have not exported an animation yet, start with the animation export reference. For the export family as a whole, see the developer overview.
Anatomy of an animated export
The exporter renders each transition between consecutive animation states into its own layer group, a g element with a sequential id like mk_1, mk_2, and so on. Layers are crossfaded by SMIL animate elements on opacity: each layer fades in when its transition window opens and fades out after the next state’s hold ends, with a near-instant crossfade so adjacent layers never visibly double-expose. When you export with looping enabled, every animate carries repeatCount="indefinite"; otherwise the timeline plays once and freezes on the last frame.
The root svg element carries three machine-readable timing attributes:
<svg id="molkit-2smsi" xmlns="http://www.w3.org/2000/svg" viewBox="-43.13 -23.01 86.25 42.32" data-duration="5.500" data-breakpoints="0.000,1.200,2.400,3.600,5.500" data-transitions="0.400,1.600,2.800,4.350">| Attribute | Meaning |
|---|---|
data-duration | Total timeline length in seconds. |
data-breakpoints | Comma-separated times at which each state has fully settled. Always starts at 0.000; one entry per state. |
data-transitions | Comma-separated times at which each transition begins (after the preceding hold). |
Everything you need to drive the animation is on that one element.
The control API
The playback controls are not Molkit inventions. They are the standard SMIL methods every SVGSVGElement exposes, so there is nothing to load and nothing version-specific to track.
| Call | What it does |
|---|---|
svg.pauseAnimations() | Freezes the timeline at the current time. |
svg.unpauseAnimations() | Resumes playback. |
svg.animationsPaused() | Returns true while paused. |
svg.setCurrentTime(t) | Seeks to t seconds. Works while paused; the frame updates immediately. |
svg.getCurrentTime() | Current document time in seconds. Monotonic: on a looping export it keeps counting past data-duration rather than wrapping. |
Seeking and pausing compose freely: pause first, then call setCurrentTime repeatedly to show any frame as a still.
Embedding for animation
How you embed decides how much of that API you actually get. The short version:
| Method | Playback | Scripting from your page |
|---|---|---|
img | Plays, but you cannot pause or seek it | None |
object | Plays | Only via same-origin contentDocument plumbing |
| Inline | Plays | Full control |
The rest of this page assumes inline embedding. See Embedding SVG Exports for the full comparison, sizing, and the duplicate-id pitfalls of inlining more than one export.
Recipe: play/pause button
The minimal control. Pause on load so the reader starts it deliberately:
const svg = document.querySelector('#stage svg');const btn = document.querySelector('#play-pause');
svg.pauseAnimations(); // start paused on the first frame
btn.addEventListener('click', () => { if (svg.animationsPaused()) { svg.unpauseAnimations(); btn.textContent = 'Pause'; } else { svg.pauseAnimations(); btn.textContent = 'Play'; }});Recipe: a scrubber
A range input that seeks the timeline, plus a requestAnimationFrame loop that reflects playback position back into the slider while the animation runs.
-
Add a slider next to the inlined SVG:
markup <div id="stage"><!-- inline animated SVG here --></div><input id="scrub" type="range" min="0" step="0.01" value="0"> -
Wire both directions:
scrubber.js const svg = document.querySelector('#stage svg');const scrub = document.querySelector('#scrub');const duration = parseFloat(svg.dataset.duration);scrub.max = duration;// Looping exports use repeatCount="indefinite" on their animationsconst loops = !!svg.querySelector('animate[repeatCount="indefinite"]');let scrubbing = false;scrub.addEventListener('pointerdown', () => {scrubbing = true;svg.pauseAnimations();});scrub.addEventListener('pointerup', () => {scrubbing = false;svg.unpauseAnimations();});scrub.addEventListener('input', () => {svg.setCurrentTime(parseFloat(scrub.value));});// Reflect playback position into the slider.// getCurrentTime() is monotonic, so wrap it for looping exports// and clamp it for one-shot exports.function tick() {if (!scrubbing) {const raw = svg.getCurrentTime();const t = loops ? raw % duration : Math.min(raw, duration);scrub.value = t.toFixed(2);}requestAnimationFrame(tick);}requestAnimationFrame(tick);
Recipe: click-to-step
data-breakpoints lists the exact times at which each state has settled, which makes PowerPoint-style stepped reveals a few lines of code: pause at time zero, then jump breakpoint to breakpoint on click or arrow keys.
const svg = document.querySelector('#stage svg');const breakpoints = svg.dataset.breakpoints.split(',').map(Number);let step = 0;
svg.pauseAnimations();svg.setCurrentTime(0);
function goTo(i) { step = Math.max(0, Math.min(i, breakpoints.length - 1)); svg.setCurrentTime(breakpoints[step]);}
svg.addEventListener('click', () => goTo(step + 1));
document.addEventListener('keydown', (e) => { if (e.key === 'ArrowRight') goTo(step + 1); if (e.key === 'ArrowLeft') goTo(step - 1);});Each click shows the next settled state as a still frame. If you want the transition to play instead of jumping, seek to the matching data-transitions entry, call unpauseAnimations(), and pause again once getCurrentTime() passes the next breakpoint.
Behavior notes
Reduced motion. Every animated export embeds a media query that hides all SMIL elements when the OS requests reduced motion, leaving the first frame as a static image:
@media (prefers-reduced-motion: reduce) { #molkit-xxxxx animate, #molkit-xxxxx animateTransform, #molkit-xxxxx animateMotion { display: none }}You do not need to handle this preference yourself, but if you build custom controls, consider disabling autoplay when matchMedia('(prefers-reduced-motion: reduce)') matches.
Dark mode. Theme-aware animated exports ship the same three-state hooks as static exports: dark rules apply under the OS prefers-color-scheme: dark media query by default, an mk-dark class on the SVG or any ancestor forces dark regardless of the OS, and an mk-light class opts out of the media query. Details and recipes live in CSS Theming.
Restart. There is no dedicated restart call; svg.setCurrentTime(0) rewinds, and the timeline keeps playing unless you also pause.
Poster pattern. Export a static SVG of the same drawing alongside the animated one and swap the two on interaction; the full recipe is in the cookbook.
See also
- Animation export reference for the export dialog, looping, and timing options
- Embedding SVG Exports for inline injection and duplicate-id handling
- CSS Theming for the dark-mode class hooks
- Cookbook for the poster/toggle swap pattern
- Developer overview for the rest of the integration surface