Skip to content

JSON Animation Schema

AnimX can describe a complete animation as a plain JSON document. Pass it to fromJSON() and you get a live, playable Timeline back.

javascript
import { fromJSON } from 'animx';

const tl = fromJSON(myDoc);
tl.play();

No separate animation code needed. Load the JSON from a server, a CMS, a database, or bundle it with your app. The format is self-contained: you can define custom easing curves, reusable variables, and full callback sequences right inside the document without writing any JavaScript for them.

Some things this is useful for: CMS-driven motion where animations are stored as content, no-code editors that serialize their state into AnimX JSON, A/B testing animation variants without a redeploy, and AnimX Studio (the visual editor's native save format).

Your first JSON animation

javascript
import { fromJSON } from 'animx';

const tl = fromJSON({
  version: "1.0",
  tweens: [
    {
      target:   ".card",
      type:     "animateFrom",
      duration: 0.6,
      ease:     "cubic.out",
      props:    { opacity: 0, y: -30 }
    }
  ]
});

tl.play();

fromJSON validates the document, processes variables and custom easings, then builds and returns a Timeline. It's the same as a timeline built with timeline(), so .play(), .pause(), .seek(), .reverse(), and .kill() all work normally.

Document shape

An AnimX JSON document is a plain object with up to seven top-level keys:

version        always "1.0"
id             optional name (for tooling and actions)
customEasings  define your own easing curves inline
variables      reusable named values ($varName)
callbacks      declarative action sequences
defaults       global tween defaults
timeline       sequenced timeline
tweens         flat concurrent tween list (shorthand)

You need at least timeline or tweens. Everything else is optional.

customEasings

You can define named easing curves directly in the document. Once defined, the name works anywhere ease is accepted: tweens, keyframes, staggers, nested timelines. Under the hood this calls defineEase() before the timeline is built.

json
{
  "customEasings": [
    { "name": "snap",    "cubicBezier": [0.34, 1.56, 0.64, 1] },
    { "name": "chunky",  "steps": [8, "end"] },
    { "name": "softer",  "from": "back.out(1.5)" }
  ],
  "tweens": [
    { "target": ".box", "ease": "snap", "props": { "x": 200 }, "duration": 0.5 }
  ]
}

Equivalent JavaScript:

javascript
import { defineEase, animate } from 'animx';

defineEase('snap',   'cubic-bezier(0.34, 1.56, 0.64, 1)');
defineEase('chunky', 'steps(8, end)');
defineEase('softer', 'back.out(1.5)');

animate('.box', { x: 200, duration: 0.5, ease: 'snap' });

Three ways to define a curve:

cubicBezier

Four numbers [x1, y1, x2, y2], same as CSS cubic-bezier(). Use any online tool to generate values.

json
{ "name": "myEase", "cubicBezier": [0.22, 1, 0.36, 1] }

steps

[count] or [count, "start" | "end"], same as CSS steps(). Good for sprite animations.

json
{ "name": "sprite", "steps": [12, "end"] }

from (alias)

References any existing built-in or parameterised ease string.

json
{ "name": "softerBack", "from": "back.out(1.2)" }

Custom ease names are registered before the validator runs, so you can use a name you just defined in the same document without any ordering constraint.

variables

Declare constants once and reference them anywhere in the document with the $ prefix. The parser resolves all $name references before building anything, so the live animation objects never see them.

json
{
  "variables": {
    "dur":    0.6,
    "slideY": -40,
    "brand":  "#ff2d6a"
  },
  "defaults": { "duration": "$dur" },
  "tweens": [
    { "target": "#hero",    "props": { "y": "$slideY", "color": "$brand" } },
    { "target": "#tagline", "props": { "y": "$slideY" }, "delay": 0.2 }
  ]
}

Equivalent JavaScript (variables become regular constants):

javascript
import { animate } from 'animx';

const dur    = 0.6;
const slideY = -40;
const brand  = '#ff2d6a';

animate('#hero',    { y: slideY, color: brand, duration: dur });
animate('#tagline', { y: slideY, duration: dur, delay: 0.2 });

Values can be numbers, strings, or booleans. Substitution is recursive, so variables work inside callbacks, keyframes, stagger objects, nested timelines, everywhere.

Expressions

You can use $varName tokens inside arithmetic expressions. The variables get substituted first, then the expression is evaluated.

json
"variables": { "slideY": 40, "dur": 0.6, "gap": 10 }
ExpressionResult
"-$slideY"-40
"$slideY + 20"60
"$slideY * 2"80
"$dur + 0.2"0.8
"($slideY + $gap) * 2"100

Supports +, -, *, /, and (). Spaces are ignored. The evaluator is a safe recursive-descent parser with no eval. If a variable name is not found in variables, it stays as-is.

callbacks

Inline callbacks let you respond to animation lifecycle events with a declarative sequence of actions, no JavaScript function required.

json
{
  "callbacks": {
    "onHeroDone": [
      { "action": "addClass",  "target": "#hero", "class": "visible" },
      { "action": "animate",   "target": "#cta",  "props": { "opacity": 1, "y": 0 }, "duration": 0.4 },
      { "action": "wait",      "seconds": 1 },
      { "action": "dispatch",  "event": "hero:visible" }
    ]
  },
  "tweens": [
    { "target": "#hero", "props": { "opacity": 1 }, "duration": 0.8, "onComplete": "onHeroDone" }
  ]
}

Each entry is an ordered array of actions that run top-to-bottom. The wait action pauses execution before the next one starts.

Callback resolution

When AnimX resolves a callback name string, it checks three places in order:

PrioritySourceExample
1callbacks in this document"onComplete": "onHeroDone"
2registerCallback() registryPre-registered JS functions
3window[name]"onComplete": "window.myFn"

The window. prefix is the escape hatch for plain <script> globals. AnimX only looks there when the name string starts with "window.".

json
"onComplete": "window.myGlobalHandler"

DOM actions

ActionWhat it doesFields
addClassAdds a CSS classtarget, class
removeClassRemoves a CSS classtarget, class
toggleClassToggles a CSS classtarget, class
setAttributeSets an HTML attributetarget, attr, value
removeAttributeRemoves an HTML attributetarget, attr
setStyleApplies inline stylestarget, style (object)
setTextSets element text contenttarget, text

Animation actions

ActionWhat it doesFields
animateRuns animate()target, props, optional duration, ease
animateFromRuns animateFrom()target, props, optional duration, ease
applyInstant property settarget, props

Timeline control actions

Target timelines by their id field.

ActionWhat it doesFields
playPlays a timelineid
pausePauses itid
stopStops and resets itid
seekSeeks to a timeid, time (seconds)
restartRestarts from the beginningid

Utility actions

ActionWhat it doesFields
waitPauses the sequenceseconds
dispatchFires a CustomEvent on windowevent, optional detail
navigateChanges window.location.hrefurl

defaults

Applied to every tween in the document. Tween-level values override them.

json
"defaults": {
  "duration":  0.5,
  "ease":      "cubic.out",
  "repeat":    0,
  "yoyo":      false,
  "overwrite": "auto"
}

timeline

The root timeline sequences tweens and nested timelines in time.

json
"timeline": {
  "id":          "mainTl",
  "paused":      true,
  "repeat":      -1,
  "yoyo":        true,
  "repeatDelay": 0.5,
  "delay":       0.2,
  "timeScale":   1.5,
  "onComplete":  "onDone",
  "labels": {
    "intro":  0,
    "middle": 1.5,
    "outro":  3.2
  },
  "tweens": [ ... ],
  "timelines": [ ... ]
}
PropertyDefaultDescription
idName used by control actions and tooling.
pausedfalseStart paused, requires a manual .play() or a play action.
repeat0Extra plays. -1 loops forever.
repeatDelay0Seconds between repeats.
yoyofalseReverse on alternating repeats.
delay0Seconds before starting.
ease"none"Global ease that warps time for all children.
timeScale1Speed multiplier. 2 = double speed.
onStart / onUpdate / onComplete / onRepeat / onReverseCompleteCallback name strings.
tweensArray of tween definitions placed in this timeline.
labelsNamed timestamps in seconds. Use as position values in tweens.
timelinesArray of { position, timeline } for nesting child timelines.

Labels

Labels are named timestamps within a timeline. You define them once and reference the name as a position in any tween.

json
"labels": { "intro": 0, "cards": 0.8, "outro": 3 }
json
{ "target": ".fade", "position": "outro", "props": { "opacity": 0 }, "duration": 0.4 }

Equivalent JavaScript:

javascript
import { animate } from 'animx';

tl.addLabel('intro', 0);
tl.addLabel('cards', 0.8);
tl.addLabel('outro', 3);
tl.add(animate('.fade', { opacity: 0, duration: 0.4 }), 'outro');

Nested timelines

Nested timelines behave like tl.add(childTimeline, position) in JavaScript.

json
"timelines": [
  { "position": "cards", "timeline": { "tweens": [ ... ] } },
  { "position": "+=0.2", "timeline": { "tweens": [ ... ] } }
]

Equivalent JavaScript:

javascript
import { timeline } from 'animx';

const child = timeline();
// ... add tweens to child
tl.add(child, 'cards');

const child2 = timeline();
tl.add(child2, '+=0.2');


## Tweens

A tween targets one or more DOM elements and animates CSS properties over time.

### target

```json
"target": "#hero"
"target": ".card"
"target": ["#a", "#b", ".group"]

Setup Actions

You can run an array of synchronous actions to prepare a target before the tween initializes using the setup property. This is especially useful for plugins like TextSlicer that modify the DOM.

json
{
  "target": ".title",
  "setup": [
    { "action": "splitText", "type": "words" }
  ],
  "duration": 0.8,
  "props": { "opacity": 0, "y": 20 },
  "stagger": 0.1
}

If a setup action handler returns a new CSS selector string (as splitText does to target the newly split character/word elements), the JSON parser will automatically rewrite the tween's target property for you. You don't need to know the internal class names the plugin generates!

type

ValueAnimatesNotes
"animate" (default)Current state to propsMost common.
"animateFrom"props to current stateGreat for entrances: the element starts from the props values and its natural position becomes the destination, so the DOM does not need to be pre-styled.
"sequence"fromProps to propsRequires both props and fromProps. Useful when both endpoints must be explicit regardless of current state.
"apply"Instant set to propsNo duration, no interpolation. Use it to snap elements into position or reset state.
json
{ "type": "animate",     "target": ".box", "props": { "x": 200 }, "duration": 0.6 }
{ "type": "animateFrom", "target": ".box", "props": { "x": 200 }, "duration": 0.6 }
{ "type": "sequence",    "target": ".box", "fromProps": { "x": 0 }, "props": { "x": 200 }, "duration": 0.6 }
{ "type": "apply",       "target": ".box", "props": { "x": 200 } }

Equivalent JavaScript:

javascript
import { animate, animateFrom, sequence, apply } from 'animx';

animate('.box',     { x: 200, duration: 0.6 });
animateFrom('.box', { x: 200, duration: 0.6 });
sequence('.box',    { x: 0  }, { x: 200, duration: 0.6 });
apply('.box',       { x: 200 });

Timing

json
{
  "target":      ".box",
  "duration":    0.8,
  "delay":       0.2,
  "repeat":      2,
  "repeatDelay": 0.4,
  "yoyo":        true
}

ease

Built-in families with .in, .out, or .inOut suffix:

quad   cubic   quart   quint
sine   expo    circ
back   elastic bounce
linear none

Or parameterised: back.out(2.5), elastic.out(1, 0.3), steps(6), cubic-bezier(0.25, 0.1, 0.25, 1.0).

Or a name from customEasings. Default: cubic.out.

Props and relative values

json
"props": {
  "x":               200,
  "y":               "+=50",
  "scale":           "*=1.2",
  "opacity":         "-=0.3",
  "backgroundColor": "#ff2d6a",
  "borderRadius":    "50%"
}

Prefix with += (add), -= (subtract), or *= (multiply) to animate relative to the current value.

Transform shorthands:

KeyCSS
x, y, ztranslateX/Y/Z
rotationrotate (degrees)
rotationX, rotationYrotateX/Y
scale, scaleX, scaleYscale()
skewX, skewYskew()

Position inside a timeline

The position syntax is identical to the second argument of tl.add() in JavaScript.

ValueMeaning
1.51.5 s from the timeline start
"+=0.5"0.5 s after the previous tween ends
"-=0.5"0.5 s before the previous tween ends (overlap)
"<"Same start as the previous tween
"<+=0.5"0.5 s after the previous tween starts
"myLabel"At a named label
json
{ "target": ".a", "props": { "x": 100 }, "duration": 1 },
{ "target": ".b", "props": { "y": 50  }, "duration": 1, "position": "-=0.3" }

Equivalent JavaScript:

javascript
import { animate } from 'animx';

tl.add(animate('.a', { x: 100, duration: 1 }));
tl.add(animate('.b', { y: 50,  duration: 1 }), '-=0.3');

Stagger

The stagger value passes directly into the AnimX stagger option. The object shape is identical in JSON and in JavaScript.

json
"stagger": 0.1

Equivalent JavaScript:

javascript
import { animate } from 'animx';

animate('.card', { x: 100, duration: 0.5, stagger: 0.1 });
json
"stagger": {
  "amount": 0.6,
  "from":   "center",
  "grid":   [4, 3],
  "axis":   "x",
  "ease":   "quad.inOut"
}

Equivalent JavaScript:

javascript
import { animate } from 'animx';

animate('.card', {
  x: 100,
  duration: 0.5,
  stagger: { amount: 0.6, from: 'center', grid: [4, 3], axis: 'x', ease: 'quad.inOut' }
});

Use amount when a fixed total spread is needed regardless of how many elements there are. Use each when a consistent per-element offset is needed no matter the count. They are mutually exclusive; each takes priority if both are provided.

PropertyDescription
amountTotal stagger time distributed across all elements.
eachPer-element delay (overrides amount).
fromOrigin: "start", "center", "end", "edges", "random", an index, or [col, row].
grid[cols, rows] for 2D distance-based stagger.
axis"x" or "y" to restrict grid stagger to one axis.
easeEase applied to the stagger distribution curve.

Keyframes

Keyframes let you animate through multiple waypoints in a single tween. The parser converts them into a chain of sub-tweens whose durations are computed from the at deltas.

json
{
  "target":   ".ball",
  "duration": 2,
  "keyframes": [
    { "at": "0%",   "props": { "x": 0,   "opacity": 0 } },
    { "at": "30%",  "props": { "x": 150, "opacity": 1 }, "ease": "cubic.out" },
    { "at": "70%",  "props": { "x": 300, "opacity": 1 }, "ease": "linear" },
    { "at": "100%", "props": { "x": 400, "opacity": 0 }, "ease": "cubic.in" }
  ]
}

Equivalent JavaScript (manual segment chain):

javascript
import { timeline, sequence } from 'animx';

// 2s total: 30% = 0.6s, 40% = 0.8s, 30% = 0.6s
const tl = timeline();
tl.add(sequence('.ball', { x: 0,   opacity: 0 }, { x: 150, opacity: 1, duration: 0.6, ease: 'cubic.out' }));
tl.add(sequence('.ball', { x: 150, opacity: 1 }, { x: 300, opacity: 1, duration: 0.8, ease: 'linear'    }));
tl.add(sequence('.ball', { x: 300, opacity: 1 }, { x: 400, opacity: 0, duration: 0.6, ease: 'cubic.in'  }));

at is a fraction 0-1 or a percentage string like "50%". Each ease controls the curve into that waypoint from the previous one. Segment durations are computed automatically.

Registering external callbacks

If you need a JavaScript function in a callback instead of a declarative action sequence, register it before calling fromJSON:

javascript
import { registerCallback } from 'animx';

registerCallback('trackEvent', () => {
  analytics.track('animation-complete');
});
json
{ "target": ".hero", "props": { "opacity": 1 }, "onComplete": "trackEvent" }

For one-off globals in a <script> tag, use the window. prefix:

html
<script>
  function onSlideIn() { document.title = 'Ready'; }
</script>
json
{ "onComplete": "window.onSlideIn" }

Loading from a file

javascript
import { fromJSON } from 'animx';

const res = await fetch('/animations/hero.json');
const doc = await res.json();
const tl  = fromJSON(doc, { paused: true });

document.querySelector('#play').onclick = () => tl.play();

Full example

json
{
  "version": "1.0",
  "id": "product-reveal",

  "customEasings": [
    { "name": "pop", "cubicBezier": [0.34, 1.56, 0.64, 1] }
  ],

  "variables": {
    "dur":  0.6,
    "rise": -50
  },

  "callbacks": {
    "onRevealDone": [
      { "action": "addClass",  "target": "#product", "class": "interactive" },
      { "action": "animate",   "target": "#buy-btn", "props": { "opacity": 1, "y": 0 }, "duration": 0.4, "ease": "pop" },
      { "action": "dispatch",  "event": "product:visible" }
    ]
  },

  "defaults": {
    "duration": "$dur",
    "ease":     "pop"
  },

  "timeline": {
    "id":         "revealTl",
    "paused":     true,
    "onComplete": "onRevealDone",
    "labels": { "details": 0.9 },
    "tweens": [
      {
        "target": "#product-image",
        "type":   "animateFrom",
        "props":  { "opacity": 0, "scale": 0.85 },
        "ease":   "cubic.out"
      },
      {
        "target":   "#product-title",
        "type":     "animateFrom",
        "props":    { "opacity": 0, "y": "$rise" },
        "position": "+=0.1"
      },
      {
        "target":   ".detail-item",
        "position": "details",
        "type":     "animateFrom",
        "duration": 0.5,
        "stagger":  0.08,
        "props":    { "opacity": 0, "x": -20 }
      }
    ]
  }
}