Appearance
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 }| Expression | Result |
|---|---|
"-$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:
| Priority | Source | Example |
|---|---|---|
| 1 | callbacks in this document | "onComplete": "onHeroDone" |
| 2 | registerCallback() registry | Pre-registered JS functions |
| 3 | window[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
| Action | What it does | Fields |
|---|---|---|
addClass | Adds a CSS class | target, class |
removeClass | Removes a CSS class | target, class |
toggleClass | Toggles a CSS class | target, class |
setAttribute | Sets an HTML attribute | target, attr, value |
removeAttribute | Removes an HTML attribute | target, attr |
setStyle | Applies inline styles | target, style (object) |
setText | Sets element text content | target, text |
Animation actions
| Action | What it does | Fields |
|---|---|---|
animate | Runs animate() | target, props, optional duration, ease |
animateFrom | Runs animateFrom() | target, props, optional duration, ease |
apply | Instant property set | target, props |
Timeline control actions
Target timelines by their id field.
| Action | What it does | Fields |
|---|---|---|
play | Plays a timeline | id |
pause | Pauses it | id |
stop | Stops and resets it | id |
seek | Seeks to a time | id, time (seconds) |
restart | Restarts from the beginning | id |
Utility actions
| Action | What it does | Fields |
|---|---|---|
wait | Pauses the sequence | seconds |
dispatch | Fires a CustomEvent on window | event, optional detail |
navigate | Changes window.location.href | url |
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": [ ... ]
}| Property | Default | Description |
|---|---|---|
id | Name used by control actions and tooling. | |
paused | false | Start paused, requires a manual .play() or a play action. |
repeat | 0 | Extra plays. -1 loops forever. |
repeatDelay | 0 | Seconds between repeats. |
yoyo | false | Reverse on alternating repeats. |
delay | 0 | Seconds before starting. |
ease | "none" | Global ease that warps time for all children. |
timeScale | 1 | Speed multiplier. 2 = double speed. |
onStart / onUpdate / onComplete / onRepeat / onReverseComplete | Callback name strings. | |
tweens | Array of tween definitions placed in this timeline. | |
labels | Named timestamps in seconds. Use as position values in tweens. | |
timelines | Array 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
| Value | Animates | Notes |
|---|---|---|
"animate" (default) | Current state to props | Most common. |
"animateFrom" | props to current state | Great 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 props | Requires both props and fromProps. Useful when both endpoints must be explicit regardless of current state. |
"apply" | Instant set to props | No 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 noneOr 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:
| Key | CSS |
|---|---|
x, y, z | translateX/Y/Z |
rotation | rotate (degrees) |
rotationX, rotationY | rotateX/Y |
scale, scaleX, scaleY | scale() |
skewX, skewY | skew() |
Position inside a timeline
The position syntax is identical to the second argument of tl.add() in JavaScript.
| Value | Meaning |
|---|---|
1.5 | 1.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.1Equivalent 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.
| Property | Description |
|---|---|
amount | Total stagger time distributed across all elements. |
each | Per-element delay (overrides amount). |
from | Origin: "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. |
ease | Ease 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 }
}
]
}
}