
Built on top of motion-svg
Export production-ready code.
The first declarative SVG animation engine built specifically for path-level control.
Parse, wrap, animate & export SVG paths with keyframes, easing curves, interaction triggers, path morphing, and a plugin system. Zero dependencies. TypeScript-first.
hover · click · loop · scroll · appear · manual
createPlayback · rate · events · time-stretching
exportBundle · importBundle · validateBundle
PluginManager · hooks pipeline · zero-overhead
MotionSvgPlayer
MotionSvgActor
useMotionSvg / useActor
<motion-svg> custom element
Shadow DOM + attributes API
registerMotionSvg()
npm install motion-svgnpm install motion-svg| Import Path | Description |
|---|---|
| motion-svg | Core engine (parser, actors, timelines, easing, bundles, plugins) |
| motion-svg/react | React components and hooks |
| motion-svg/vanilla | <motion-svg> Web Component (framework-free) |
| motion-svg/plugins | Official plugins (spring physics, custom easing) |
Works in Node.js and browsers. Zero dependencies. TypeScript types included.
Minimal flow from SVG string to animated, exportable bundle:
import { parseSvg, createActor, timeline, trigger, exportBundle } from 'motion-svg';
// 1. Parse an SVG
const scene = parseSvg('<svg viewBox="0 0 200 200"><path id="star" d="M100,10 L..." fill="#f00"/></svg>');
// 2. Create an Actor from a path
const star = createActor({
id: 'star',
paths: [scene.paths[0]],
origin: { x: 100, y: 100 },
});
// 3. Define keyframes
const anim = timeline(star, {
keyframes: [
{ at: 0, scale: 1, rotation: 0 },
{ at: 600, scale: 1.3, rotation: 180, curve: 'easeInOutBack' },
{ at: 1200, scale: 1, rotation: 360, curve: 'easeOutCubic' },
],
});
// 4. Set a trigger
const triggerBinding = trigger(anim, { type: 'hover', reverse: true });
// 5. Export as a self-contained JSON bundle
const json = exportBundle({
scene,
actors: [star],
timelines: [anim],
triggers: [triggerBinding],
});import { MotionSvgPlayer } from 'motion-svg/react';
import bundle from './animation.motionsvg.json';
function App() {
return <MotionSvgPlayer data={bundle} width={400} height={300} />;
}Parse an SVG string into a structured Scene object. Regex-based — works in Node.js and browsers, no DOM required.
import { parseSvg } from 'motion-svg';
const scene = parseSvg(svgString);
// scene.viewBox → { x, y, w, h }
// scene.paths → SvgPath[] (all <path>, <rect>, <circle>, etc.)
// scene.groups → SvgGroup[] (all <g> with children)
// scene.colors → Record<id, color>
// scene.gradients → GradientDef[] (linear + radial)
// scene.metadata → { xmlns, width, height, originalSvg }| Field | Type | Description |
|---|---|---|
| paths | SvgPath[] | All <path> elements with id, d, fill, stroke, transform |
| groups | SvgGroup[] | All <g> elements with child hierarchy |
| viewBox | ViewBox | Canvas dimensions { x, y, w, h } |
| colors | ColorMap | Map of element id → color value |
| gradients | GradientDef[] | Linear and radial gradient definitions |
| metadata | SvgMetadata | Original xmlns, width, height, raw SVG |
Create an Actor — a controllable wrapper around one or more SVG paths with independent transform properties.
import { createActor } from 'motion-svg';
const actor = createActor({
id: 'my-actor',
paths: [scene.paths[0], scene.paths[1]],
origin: { x: 100, y: 100 },
z: 1, // optional z-order
});| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier |
| paths | SvgPath[] | SVG paths to include |
| origin | Point | Transform origin { x, y } |
| z | number? | Z-order for stacking (default: 0) |
Generate an SVG path d string for a geometric shape. Bounding box is always [0, 0, width, height].
import { generateShapePath } from 'motion-svg';
const rect = generateShapePath('rect', 100, 50);
const hex = generateShapePath('polygon', 80, 80, { sides: 6 });Supported shapes: 'rect' | 'ellipse' | 'line' | 'arrow' | 'polygon'
Create a keyframe timeline for an actor. Keyframes are sorted by at time and the first keyframe is auto-filled with actor defaults.
import { timeline } from 'motion-svg';
const tl = timeline(actor, {
keyframes: [
{ at: 0, opacity: 0, position: { x: 0, y: 20 } },
{ at: 400, opacity: 1, position: { x: 0, y: 0 }, curve: 'easeOutCubic' },
],
});
// tl.id, tl.actorId, tl.keyframes, tl.duration| Property | Type | Default | Description |
|---|---|---|---|
| at | number | required | Time in milliseconds |
| position | Point | actor origin | { x, y } position |
| scale | number | Point | 1 | Uniform or { x, y } scale |
| rotation | number | 0 | Rotation in degrees |
| opacity | number | 1 | Opacity 0..1 |
| fill | string | original | Fill color or url(#gradientId) |
| stroke | string | original | Stroke color or gradient ref |
| strokeWidth | number | original | Stroke width |
| strokeAlign | StrokeAlign | 'center' | 'center' | 'inside' | 'outside' |
| blurRadius | number | 0 | Gaussian blur in px |
| backdropBlur | number | 0 | Backdrop blur in px |
| width | number | — | Shape width (shape actors) |
| height | number | — | Shape height (shape actors) |
| pathD | string | — | SVG path d for morphing |
| curve | EasingCurve | 'linear' | Easing to reach this keyframe |
20+ built-in easing functions plus custom cubic bezier. Every keyframe can specify a curve that controls the transition from the previous keyframe.
| Category | Functions |
|---|---|
| Linear | linear |
| Sine | easeIn, easeOut, easeInOut |
| Quad | easeInQuad, easeOutQuad, easeInOutQuad |
| Cubic | easeInCubic, easeOutCubic, easeInOutCubic |
| Quart | easeInQuart, easeOutQuart, easeInOutQuart |
| Back | easeInBack, easeOutBack, easeInOutBack |
| Elastic | easeInElastic, easeOutElastic, easeInOutElastic |
| Bounce | easeInBounce, easeOutBounce, easeInOutBounce |
import { cubicBezier, getEasingFunction } from 'motion-svg';
// Custom cubic bezier (like CSS cubic-bezier)
const myEasing = cubicBezier(0.25, 0.1, 0.25, 1.0);
// Resolve by name
const fn = getEasingFunction('easeOutBack'); // (t: number) => numberCustom cubic bezier in keyframes:
{ at: 500, curve: { type: 'cubicBezier', x1: 0.4, y1: 0, x2: 0.2, y2: 1 } }Chain multiple timelines sequentially into a single Timeline.
import { sequence } from 'motion-svg';
const combined = sequence(actor, {
items: [
{ timeline: fadeIn },
{ timeline: slideUp, delay: 100 },
{ timeline: bounce, offset: 800 },
],
});Create staggered timelines for multiple actors with the same animation.
import { stagger } from 'motion-svg';
const timelines = stagger({
actors: [card1, card2, card3, card4],
keyframes: [
{ at: 0, opacity: 0, position: { x: 0, y: 20 } },
{ at: 400, opacity: 1, position: { x: 0, y: 0 }, curve: 'easeOutCubic' },
],
stagger: 100, // 100ms between each actor
from: 'start', // 'start' | 'end' | 'center' | 'edges'
});Return the total duration of timelines running in parallel (max duration).
Interpolate SVG path d attributes between shapes. Automatically normalizes all commands to cubic beziers and balances segment counts.
import { lerpPath } from 'motion-svg';
const mid = lerpPath(
'M0,0 L10,0 L10,10 Z', // triangle
'M0,0 L10,0 L10,10 L0,10 Z', // square
0.5
);Use pathD in keyframes for animated morphing:
const morph = timeline(actor, {
keyframes: [
{ at: 0, pathD: circlePath },
{ at: 1000, pathD: starPath, curve: 'easeInOutCubic' },
],
});Helpers: parsePathD(d), normalizeToCubic(commands), balanceCommands(a, b). Supports all SVG path commands: M, L, H, V, C, S, Q, T, A, Z.
Bind a trigger to a timeline. The actual event wiring is handled by the renderer.
import { trigger } from 'motion-svg';
trigger(tl, { type: 'hover', reverse: true });
trigger(tl, { type: 'click', toggle: true });
trigger(tl, { type: 'loop', iterations: Infinity, direction: 'alternate', delay: 200 });
trigger(tl, { type: 'scroll', start: 0.2, end: 0.8 });
trigger(tl, { type: 'appear', threshold: 0.5, once: true });
trigger(tl, { type: 'manual' });| Trigger | Key Options |
|---|---|
| hover | reverse?: boolean |
| click | toggle?: boolean |
| loop | iterations?, direction?: 'normal'|'reverse'|'alternate', delay? |
| scroll | start?: number, end?: number (0..1 viewport %) |
| appear | threshold?: number, once?: boolean |
| manual | — (controlled via PlaybackController) |
Runtime playback controller that drives animations frame-by-frame using requestAnimationFrame.
import { createPlayback } from 'motion-svg';
const ctrl = createPlayback({
timeline: tl,
trigger: triggerBinding,
gradients: scene.gradients,
onUpdate: (state, timeMs) => { /* apply state to DOM */ },
onComplete: () => { /* all done */ },
});
ctrl.play();
ctrl.pause();
ctrl.stop();
ctrl.seek(500);
ctrl.reverse();| Property | Type | Description |
|---|---|---|
| state | PlaybackState | 'idle' | 'playing' | 'paused' | 'finished' |
| currentTime | number | Current time in ms |
| duration | number | Timeline duration in ms |
| progress | number | Normalized progress 0..1 |
| playbackRate | number | 1=normal, 0.5=half, 2=double, negative=reverse |
const unsub = ctrl.on('complete', (event) => {
console.log(event.progress, event.currentTime);
});
// Event types: 'start' | 'play' | 'pause' | 'stop' | 'seek'
// | 'reverse' | 'frame' | 'complete' | 'repeat'Compute interpolated state at a given time. Each property is interpolated independently — a fill-only keyframe won't create a "hold" segment for position or scale.
import { getActorStateAtTime } from 'motion-svg';
const state = getActorStateAtTime(tl, 250, { gradients: scene.gradients });
// state.position → { x: 50, y: 0 }
// state.fill → '#807f00' (smooth color lerp)Interpolate between two gradient definitions (linear/radial, cross-type).
interface ActorState {
position: Point;
scale: number | Point;
rotation: number;
opacity: number;
fill?: string;
stroke?: string;
strokeWidth?: number;
strokeAlign?: StrokeAlign;
blurRadius?: number;
backdropBlur?: number;
width?: number;
height?: number;
fillGradient?: GradientDef;
strokeGradient?: GradientDef;
pathD?: string;
}Smoothly interpolates colors and gradients between keyframes. Supports hex-to-hex, gradient-to-gradient, and color-to-gradient transitions.
// Hex → Hex
{ at: 0, fill: '#ff0000' }
{ at: 1000, fill: '#0000ff', curve: 'easeInOut' }
// At t=500 → '#800080'
// Gradient → Gradient
{ at: 0, fill: 'url(#sunrise)' }
{ at: 1000, fill: 'url(#sunset)', curve: 'easeInOut' }
// Color ↔ Gradient
{ at: 0, fill: '#ff6b6b' }
{ at: 1000, fill: 'url(#sunset)', curve: 'easeOut' }Keyframes can animate stroke alongside all other properties. Three alignment modes are supported:
| Alignment | Description |
|---|---|
| center | Default — centered on path edge |
| inside | Inside path, using clip-path |
| outside | Outside path, using paint-order |
{ at: 0, stroke: '#ff0000', strokeWidth: 2, strokeAlign: 'center' }
{ at: 500, stroke: '#00ff00', strokeWidth: 6, strokeAlign: 'outside', curve: 'easeOut' }Export a complete animation as a self-contained .motionsvg.json string.
import { exportBundle } from 'motion-svg';
const json = exportBundle({
scene,
actors: [actor1, actor2],
timelines: [tl1, tl2],
triggers: [trigger1],
variants: [{ name: 'idle', actorIds: ['a1'], timelineIndices: [0], triggerIndices: [0] }],
});Import a bundle and reconstruct all objects.
import { importBundle, getVariant } from 'motion-svg';
const imported = importBundle(jsonString);
// imported.scene, imported.actors, imported.timelines, imported.triggers, imported.variants
const variant = getVariant(imported, 'idle');
// variant.actors, variant.timelines, variant.triggers — or nullValidate a bundle JSON string without importing it.
{
"version": "1.1",
"scene": { "viewBox": {...}, "paths": [...], "gradients": [...] },
"actors": [{ "id": "logo", "pathIds": [...], "origin": {...} }],
"timelines": [{ "actorId": "logo", "keyframes": [...] }],
"triggers": [{ "timelineIdx": 0, "type": "hover", "reverse": true }],
"variants": [{ "name": "idle", "actorIds": [...], "timelineIndices": [...] }]
}A variant is a named configuration of the animation scene. Each variant specifies which actors are visible, which timelines are active, and which triggers fire. Use variants for multiple states in a single bundle — e.g. idle, loading, success.
| Field | Type | Description |
|---|---|---|
| name | string | Unique display name (used as variant prop) |
| actorIds | string[]? | Actor IDs visible. Omit = all actors. |
| timelineIndices | number[] | Indices into timelines[] array |
| triggerIndices | number[] | Indices into triggers[] array |
import { importBundle, getVariant } from 'motion-svg';
const imported = importBundle(jsonString);
const names = imported.variants.map(v => v.name);
const idle = getVariant(imported, 'idle');
if (idle) {
idle.actors; // Actor[]
idle.timelines; // Timeline[]
idle.triggers; // TriggerBinding[]
}Bundles without variants (v1.0) work unchanged. The variants field is optional and defaults to an empty array.
High-performance external store for batching actor state updates. Uses queueMicrotask to notify subscribers once per frame instead of per-actor.
import { AnimationStore } from 'motion-svg';
const store = new AnimationStore();
store.set('actor-1', state);
store.setMany({ 'actor-1': state1, 'actor-2': state2 });
const snapshot = store.getSnapshot();
const actorState = store.getActorState('actor-1');
const unsub = store.subscribe(() => { /* re-render */ });
store.reset();Zero-overhead hook-based plugin system. Plugins compose in a pipeline where each receives the output of the previous one.
import { plugins } from 'motion-svg';
plugins.register(myPlugin);
plugins.unregister('my-plugin');
plugins.listPlugins(); // [{ name, version }]
plugins.clear();| Hook | Signature | Called When |
|---|---|---|
| afterParse | (scene) => Scene | After SVG parsing |
| afterCreateActor | (actor, scene) => Actor | After actor creation |
| beforeInterpolate | (keyframes, timeMs) => Keyframe[] | Before each frame interpolation |
| afterInterpolate | (state, actorId, timeMs) => ActorState | After each frame interpolation |
| beforeExport | (bundle) => Bundle | Before bundle export |
| afterImport | (scene, actors, timelines, triggers) => {...} | After bundle import |
| interpolateProperty | (propName, from, to, t) => unknown | Custom property interpolation |
import type { MotionSvgPlugin } from 'motion-svg';
const myPlugin: MotionSvgPlugin = {
name: 'my-plugin',
version: '1.0.0',
hooks: {
afterInterpolate(state, actorId, timeMs) {
return { ...state, opacity: state.opacity * 0.8 };
},
},
destroy() { /* cleanup */ },
};Damped harmonic oscillator that adds natural overshoot and settling to scale, position, and rotation.
import { springPlugin } from 'motion-svg/plugins';
import { plugins } from 'motion-svg';
plugins.register(springPlugin({
stiffness: 120, // higher = snappier (default: 100)
damping: 8, // higher = less oscillation (default: 10)
mass: 1, // higher = more inertia (default: 1)
properties: ['scale', 'position'],
}));springResponse(t, stiffness, damping, mass): number — low-level spring response function for direct use.
Register named easing functions usable in keyframes.
import { customEasingPlugin, steppedEasing, elasticEasing } from 'motion-svg/plugins';
import { plugins } from 'motion-svg';
plugins.register(customEasingPlugin({
easings: {
stepped5: steppedEasing(5),
superElastic: elasticEasing(1.5, 0.3),
myCustom: (t) => t * t * (3 - 2 * t), // smoothstep
},
}));
// Then use in keyframes:
// { at: 500, scale: 2, curve: 'stepped5' }| Factory | Description |
|---|---|
| steppedEasing(steps) | Discrete step animation |
| slowMotionEasing(factor?) | Slow in the middle |
| elasticEasing(amplitude?, period?) | Configurable elastic |
| springEasing(overshoot?) | Spring-like back easing |
Import from motion-svg/react:
import {
MotionSvgPlayer, // Full player component
MotionSvgActor, // Single actor renderer
MotionSvgCanvas, // SVG canvas wrapper
useMotionSvg, // Control hook
useActorState, // Granular actor state hook
} from 'motion-svg/react';React hook for controlling animations. Uses useSyncExternalStore for optimal batched rendering.
const {
data, // ImportedBundle | null
actors, // Actor[]
timelines, // Timeline[]
triggers, // TriggerBinding[]
actorStates, // Record<string, ActorState>
controllers, // PlaybackController[]
play, pause, stop, seek,
playing, // boolean
variantNames, // string[]
} = useMotionSvg(bundleJson, { variant: 'idle' });Granular hook that re-renders only when a specific actor's state changes.
<MotionSvgPlayer data={bundle} width={400} height={300} variant="idle" />Renders a single actor with its current interpolated state. Supports stroke alignment, inline gradients, blur filters, and path morphing.
Register the <motion-svg> custom element. Call once before using in HTML.
import { registerMotionSvg } from 'motion-svg/vanilla';
registerMotionSvg(); // or registerMotionSvg('my-player')| Attribute | Description |
|---|---|
| src | URL to a .motionsvg.json file (fetched automatically) |
| data | Inline JSON string of the bundle |
| variant | Name of the variant to activate |
| autoplay | Start animation on load (boolean attribute) |
| width | CSS width (default: '100%') |
| height | CSS height (default: '100%') |
const el = document.querySelector('motion-svg');
el.bundle = bundleObject; // set bundle programmatically
el.variant = 'dark'; // switch variant
el.play();
el.pause();
el.stop();
el.seek(500);
el.controllers; // PlaybackController[]Events: motionsvg:ready, motionsvg:play, motionsvg:complete
After exporting a bundle, you can import and play animations in any JavaScript/TypeScript project.
import { importBundle } from 'motion-svg';
const json = await fetch('/my-animation.motionsvg.json').then(r => r.text());
const bundle = importBundle(json);import { useState, useEffect, useRef, useMemo } from 'react';
import { importBundle, getActorStateAtTime } from 'motion-svg';
function AnimatedSvg({ bundleJson }) {
const bundle = useMemo(() => importBundle(bundleJson), [bundleJson]);
const [time, setTime] = useState(0);
const rafRef = useRef();
useEffect(() => {
const start = performance.now();
const tick = () => {
setTime((performance.now() - start) % bundle.timelines[0].duration);
rafRef.current = requestAnimationFrame(tick);
};
rafRef.current = requestAnimationFrame(tick);
return () => cancelAnimationFrame(rafRef.current);
}, [bundle]);
const { scene, actors, timelines } = bundle;
return (
<svg viewBox={`${scene.viewBox.x} ${scene.viewBox.y} ${scene.viewBox.w} ${scene.viewBox.h}`}>
{actors.map(actor => {
const tl = timelines.find(t => t.actorId === actor.id);
const state = tl ? getActorStateAtTime(tl, time, { gradients: scene.gradients }) : null;
const ox = actor.origin.x, oy = actor.origin.y;
const transform = state
? `translate(${state.position.x},${state.position.y}) translate(${ox},${oy}) rotate(${state.rotation}) scale(${typeof state.scale === 'number' ? state.scale : state.scale.x}) translate(${-ox},${-oy})`
: '';
return (
<g key={actor.id} transform={transform} opacity={state?.opacity ?? 1}>
{actor.paths.map(p => <path key={p.id} d={p.d} fill={state?.fill ?? p.fill} />)}
</g>
);
})}
</svg>
);
}import { getActorStateAtTime } from 'motion-svg';
const tl = bundle.timelines[0];
let startTime = performance.now();
function animate() {
const elapsed = performance.now() - startTime;
const timeMs = elapsed % tl.duration;
const state = getActorStateAtTime(tl, timeMs, {
gradients: bundle.scene.gradients,
});
// Apply state to DOM / canvas / WebGL / React state...
requestAnimationFrame(animate);
}
animate();All types are exported from the main entry point:
import type {
// Core
Point, ViewBox, SvgPath, SvgGroup, Scene, SvgMetadata,
// Color & Gradients
ColorMap, ColorEntry, GradientStop, GradientDef,
LinearGradientDef, RadialGradientDef,
// Actor
Actor, ActorConfig, ShapeType,
// Animation
Keyframe, TimelineConfig, Timeline, EasingName,
EasingCurve, CubicBezierCurve,
// Trigger
TriggerType, TriggerConfig, TriggerBinding, LoopDirection,
HoverTrigger, ClickTrigger, LoopTrigger, ScrollTrigger,
AppearTrigger, ManualTrigger,
// Bundle
Bundle, BundleScene, BundleActor, BundleTimeline,
BundleTrigger, BundleVariant, ExportConfig,
// Playback
PlaybackState, PlaybackController, PlaybackEventType,
PlaybackEvent, PlaybackEventHandler,
// Interpolation
ActorState, InterpolateOptions,
// Bundle I/O
ImportedBundle, ValidationResult,
// React
UseMotionSvgOptions, MotionSvgInstance,
// Path Morphing
PathCommand, CubicSegment, NormalizedPath,
// Orchestration
SequenceConfig, SequenceItem, StaggerConfig, StaggerFrom,
// Plugins
MotionSvgPlugin, MotionSvgHooks, SpringConfig,
SpringProperty, CustomEasingConfig,
} from 'motion-svg';motion-svg-kit is the visual editor and developer toolkit built on top of the motion-svg core. It provides a full GUI for designing, previewing and exporting SVG animations — no code required.
Contribute to the core, get access to the kit
motion-svg-kit will be available to developers who contribute to the motion-svg core library. Contributors get early access to the visual editor, shape tools, timeline UI, and export pipeline.
| Feature | Description |
|---|---|
| Visual Timeline | Drag-and-drop keyframe editor with curve preview |
| Canvas Editor | Direct manipulation of actors, origins, and paths on an SVG canvas |
| Shape Tools | Built-in shape generator (rect, ellipse, polygon, arrow, line) |
| Live Preview | Real-time animation preview with trigger simulation |
| Export Pipeline | One-click export to .motionsvg.json bundles |
| Plugin Panel | Visual configuration for spring physics, custom easing, and more |
| Variant Manager | Create and switch between animation variants visually |
Contribute to the motion-svg core — bug fixes, features, documentation, or tests. Once your PR is merged, you'll receive access to the motion-svg-kit editor.
# 1. Fork & clone
git clone https://github.com/anthropics/motion-svg.git
cd motion-svg && pnpm install
# 2. Run tests
pnpm test
# 3. Make your contribution & open a PR
motion-svg — Declarative SVG Animation Engine · MIT