Scripting Animated SVGs
An animated molecule export is one self-contained SVG file driven by one SMIL timeline. It ships no JavaScript and needs no runtime library; the browser plays it on its own. Because SMIL runs on the standard SVG document timeline, the host page can also script it: pause it, scrub it, or turn it into a click-to-advance stepped reveal. The sections below walk through the file anatomy, the control API, and three copy-paste recipes. LaTeX solution animations are built differently; see LaTeX solution exports at the end.
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.
Each transition block is: a hold on the starting state, an optional radical-rotation phase, the transition itself, and a hold on the resulting state. Timing depends on what happens in the step (more travel, more bond changes, or a transfer takes longer), and a state holds until its arrows have finished drawing plus a short pause. The first state holds for 1 second and the last for 2 seconds. Read the times from the root attributes instead of assuming a fixed step length.
The root svg element carries the identity and timing attributes:
<svg id="molkit-2smsi" xmlns="http://www.w3.org/2000/svg" viewBox="-43.13 -23.01 86.25 42.32" data-mk-animation="true" data-mk-animation-format="smil" data-mk-animation-states="5" 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-mk-animation | true on every animated export. Use it to tell an animation from a static structure SVG. |
data-mk-animation-format | smil. |
data-mk-animation-states | Number of states in the timeline. |
data-mk-animation-loop | true when exported with looping; absent otherwise. |
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 and any radical-rotation phase. |
data-mk-effects | JSON object recording the animation effect options the file was rendered with (for example colour pulse, bond-break fragments, lone-pair and bond morphing, drift). Present when any options were set. Informational: it lets Molkit restore the same options, and its keys are not a stable contract for players. |
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 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 molecule 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. If you build custom controls, consider also 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.
Element colours inside labels. In a theme-aware animated export, a span inside a rich label (for example the O of a CH3O label) whose text is one element symbol and whose colour is that element’s palette colour loses its inline colour and gets the class mk-<Element>, such as mk-O. It then follows the same element rules as atom labels, including dark mode and any host override of .mk-O. Spans in other colours keep them.
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.
LaTeX solution exports
A LaTeX solution animation exports differently. Its file contains no SMIL elements: every line, term and strike-through is driven by CSS @keyframes in a style block inside the SVG, all sharing one duration with no delays. That changes how you script it:
pauseAnimations(),setCurrentTime()and the other SMIL calls above have no effect. Use the Web Animations API instead:svg.getAnimations({ subtree: true })returns the running CSS animations, and you can pause them or set theircurrentTime. For a simple play/pause toggle, set CSSanimation-play-stateon the descendants.- The root element has the class
mkit,data-molkit-format="latex-anim-1.0", and the samedata-duration,data-breakpoints(each line complete) anddata-transitions(each transition start) attributes as a molecule export, so the stepping logic in the recipes above carries over once you swap the seek call. It also hasrole="img"and anaria-label. Its width is given in em and it has no height, so it scales with the surrounding text. - Captions:
data-mk-captions(plain) anddata-mk-captions-htmlhold one caption per pause, anddata-mk-captions-spokenholds the version a screen reader reads. A step that pauses on several values lists those pauses indata-mk-substeps, as fractions of the time between the transition and the next breakpoint; captions are matched one to one with breakpoints plus substeps.data-mk-start-spokenis the summary read before playback. - Colour: the ink is
var(--mk-text-color, currentColor), so the file takes the text colour of the page it sits on, or the colour your page sets in--mk-text-color. A\colorin the LaTeX source still wins. A finished line rests at--mk-dim-opacity(0.6 by default) until the last pause. - The file styles two classes on the root for players:
mk-pausedsets every descendant animation toanimation-play-state: paused, andplayingsets it torunning. - Under reduced motion, a file with neither class removes its animations and shows the final frame, the complete solution. When a player has set
mk-pausedorplaying, the animations stay defined so the player can still step through them. Printing always shows the finished derivation with every line at full ink.
const svg = document.querySelector('svg.mkit');const anims = svg.getAnimations({ subtree: true });anims.forEach(a => a.pause());// jump to the second settled step, in millisecondsconst t = parseFloat(svg.dataset.breakpoints.split(',')[1]) * 1000;anims.forEach(a => { a.currentTime = t; });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