Appearance
Examples
Each example is a complete script and its rendered result. The code shown is the exact script that produced the video. Open any of them in the Studio to preview and edit them live.
A title that rises in
ts
composition({ width: 1280, height: 720, fps: 30, duration: "2.5s", background: "#0E1116" }, (comp) => {
const [cx, cy] = comp.center;
comp.add(text("Hello, Wiggle", { size: 84, weight: 700, color: "#FFFFFF" }))
.x(cx).y(cy)
.enter.rise({ by: 24, ease: { damping: 200 } });
});A shape with a gradient
ts
composition({ width: 1280, height: 720, fps: 30, duration: "2.5s", background: "#0E1116" }, (comp) => {
const [cx, cy] = comp.center;
comp.add(rect({ size: [360, 220], radius: 18, fill: gradient("#3B82F6", "#9333EA") }))
.x(cx).y(cy)
.enter.pop({ from: 0.6, ease: { damping: 12 } });
});A staggered word reveal
ts
composition({ width: 1280, height: 720, fps: 30, duration: "3s", background: "#F2F2F3" }, (comp) => {
const [cx, cy] = comp.center;
comp.add(text("Compose once. Switch in a click.", {
size: 64, weight: 800, color: "#1B1535", tracking: -2, maxWidth: 900,
}))
.x(cx).y(cy)
.words().enter.rise({ by: 22, ease: { damping: 200 } }).stagger("0.06s");
});Scale and rotate at once
Animations on different properties run together.
ts
composition({ width: 1280, height: 720, fps: 30, duration: "3s", background: "#0E1116" }, (comp) => {
const [cx, cy] = comp.center;
// Scale and rotate at once, then nothing else.
comp.add(rect({ size: 180, radius: 16, fill: "#22D3EE" }))
.x(cx).y(cy)
.scaleTo(1.6, { at: "0.4s", duration: "1.2s" })
.rotateTo(180, { at: "0.4s", duration: "1.2s" });
});Scale, then rotate
Animations on different properties are sequenced by their start times. The scale runs first and holds while the rotation waits its turn.
ts
composition({ width: 1280, height: 720, fps: 30, duration: "3s", background: "#0E1116" }, (comp) => {
const [cx, cy] = comp.center;
// Different properties, sequenced by their start times: scale up first,
// then rotate. The scale holds while the rotation waits its turn.
comp.add(rect({ size: 180, radius: 16, fill: "#22D3EE" }))
.x(cx).y(cy)
.scaleTo(1.6, { at: "0.4s", duration: "0.7s" })
.rotateTo(90, { at: "1.4s", duration: "0.7s" });
});Easing: a curve or a spring
Every place that animates takes one ease parameter. It is either a named curve ("linear" | "in" | "out" | "inOut") or a spring ({ stiffness?, damping?, mass? }). A curve eases over its duration; a spring overshoots and settles, settling naturally when no duration is given. The same ease applies to keyframes, moveTo / scaleTo / rotateTo, interpolate, and the entrance verbs.
ts
composition({ width: 1280, height: 720, fps: 30, duration: "3s", background: "#0E1116" }, (comp) => {
const [cx, cy] = comp.center;
// One `ease` parameter, two kinds. A named curve eases over its duration; a
// spring overshoots and settles (with no duration it settles naturally). The
// same `ease` works on keyframes, the tween verbs, interpolate, and the
// entrance verbs.
comp.add(text("curve", { size: 36, weight: 700, color: "#94A3B8" })).x(cx - 300).y(cy - 150);
comp.add(rect({ size: 150, radius: 16, fill: "#22D3EE" }))
.x(cx - 300).y(cy)
.rotateTo(360, { at: "0.4s", duration: "1.6s", ease: "inOut" });
comp.add(text("spring", { size: 36, weight: 700, color: "#94A3B8" })).x(cx + 300).y(cy - 150);
comp.add(rect({ size: 150, radius: 16, fill: "#E0604F" }))
.x(cx + 300).y(cy)
.rotateTo(360, { at: "0.4s", ease: { stiffness: 120, damping: 8 } });
});Moves in sequence
Several tweens on the same property chain, each continuing from where the last ended.
ts
composition({ width: 1280, height: 720, fps: 30, duration: "3.5s", background: "#0E1116" }, (comp) => {
const cy = comp.height / 2;
// Each move continues from where the last ended.
comp.add(ellipse({ size: 90, fill: "#E0604F" }))
.x(240).y(cy)
.moveTo(1040, cy, { at: "0.6s", duration: "0.9s" })
.moveTo(1040, cy - 200, { at: "1.8s", duration: "0.9s" });
});Rotating around a corner
anchor(x, y) sets the pivot for rotation and scale, as a fraction of the layer's bounds (the default is the center, 0.5, 0.5). With the pivot at the bottom-center, the bar swings from its base like a metronome rather than spinning around its middle. In the studio the pivot is drawn as a crosshair on the selected layer and can be dragged.
ts
composition({ width: 1280, height: 720, fps: 30, duration: "3s", background: "#0E1116" }, (comp) => {
const [cx, cy] = comp.center;
// anchor() sets the pivot for rotation and scale, as a fraction of the
// layer's bounds. Here the pivot is the bottom-center, so the bar swings from
// its base like a metronome instead of spinning around its middle.
comp.add(rect({ size: [44, 240], radius: 10, fill: "#E0604F" }))
.anchor(0.5, 1)
.x(cx).y(cy + 120)
.rotateTo(38, { at: "0.2s", duration: "0.8s" })
.rotateTo(-38, { at: "1.1s", duration: "0.9s" })
.rotateTo(0, { at: "2.1s", duration: "0.7s" });
});A group and its children, animated separately
A group has the same properties and tweens as a node, and its transform composes onto its children. Here the group turns one way while each child turns the other, so they orbit the center while staying upright. A child's world rotation is its own plus the parent's.
ts
composition({ width: 1280, height: 720, fps: 30, duration: "3s", background: "#0E1116" }, (comp) => {
const [cx, cy] = comp.center;
// The group turns one way; each child turns the other. A child's world
// rotation is its own plus the parent's, so the squares orbit the center
// while staying upright.
const ring = comp.group({ x: cx, y: cy });
ring.add(rect({ size: 120, radius: 12, fill: "#E0604F" })).x(-200).y(0).rotateTo(360, { duration: "3s" });
ring.add(rect({ size: 120, radius: 12, fill: "#22D3EE" })).x(200).y(0).rotateTo(360, { duration: "3s" });
ring.rotateTo(-360, { duration: "3s" });
});A window tilted in perspective
A drawn layer tilts in space with rotationX or rotationY and a perspective distance. Here the tilt eases from steep to shallow while the layer slides in.
ts
composition({ width: 1280, height: 720, fps: 30, duration: "3s", background: "#0B1020" }, (comp) => {
const cy = comp.height / 2;
comp.add(text("Show your app", { size: 72, weight: 800, color: "#FFFFFF", align: "left", tracking: -2, maxWidth: 480 }))
.x(96).y(cy - 70)
.enter.rise({ by: 26, ease: { damping: 200 } });
comp.add(text("at any angle.", { size: 72, weight: 800, color: "#38BDF8", align: "left", tracking: -2 }))
.x(96).y(cy + 10)
.at("0.12s").enter.rise({ by: 26, ease: { damping: 200 } });
comp.add(text("A perspective tilt, swinging as it plays.", { size: 26, weight: 500, color: "#94A3B8", align: "left", maxWidth: 460 }))
.x(96).y(cy + 110)
.at("0.3s").enter.fade();
// The panel slides in, then swings through perspective. The wide swing and the
// close camera make the tilt obvious.
comp.add(rect({ size: [640, 430], radius: 22, fill: gradient("#1D4ED8", "#0EA5E9") }))
.x(comp.width + 320).y(cy)
.perspective(820)
.rotationY.keys([
{ at: "0s", to: 52 },
{ at: "1.5s", to: -22, ease: "inOut" },
{ at: "3s", to: 52, ease: "inOut" },
])
.moveTo(comp.width - 300, cy, { at: "0s", duration: "1s" });
});A reusable component, driven by data
component(name, build) turns a builder into a reusable piece. Here one Row component is instantiated once per data row: each call returns a fresh group it lays out and slides in. The parts are named with name(...), so they show in the layer list and timeline and can be reached with find(...) (the leader's score gets an extra pop). Change the data and the whole graphic changes.
ts
// A reusable component: one function that builds a row, instantiated per data
// row. `component(name, build)` names every instance, so the Studio shows "Row"
// lanes; `group()` returns a detached group the caller adds and positions; and
// `name(...)` labels the parts so they are reachable with `find`.
const Row = component(
"Row",
({ rank, name, score }: { rank: number; name: string; score: number }) => {
const g = group();
g.add(rect({ size: [560, 64], radius: 12, fill: "#1B1535" })).name("plate");
g.add(text(`${rank}`, { size: 28, weight: 800, color: "#7C5CFF" })).x(-250).name("rank");
g.add(text(name, { size: 30, weight: 600, color: "#FFFFFF", align: "left" })).x(-190).name("name");
g.add(text(`${score}`, { size: 30, weight: 700, color: "#2BD4A8", align: "right" })).x(250).name("score");
g.enter.slide({ from: "left", by: 90, ease: { damping: 16 } });
return g;
},
);
const data = [
{ rank: 1, name: "Aria", score: 980 },
{ rank: 2, name: "Bo", score: 910 },
{ rank: 3, name: "Cyan", score: 870 },
];
composition(
{ width: 1280, height: 720, fps: 30, duration: "3s", background: "#0E1116" },
(comp) => {
const [cx, cy] = comp.center;
data.forEach((s, i) => {
const row = comp.add(Row(s)).x(cx).y(cy - 90 + i * 80).at(`${0.15 * i}s`);
// The parts are reachable by name: give the leader's score an extra pop.
if (i === 0) row.find("score").enter.pop({ from: 0.4 }).after(row, "0.3s");
});
},
);Blur, as a layer effect
blur softens a layer's own pixels (here a soft glow), and backdropBlur frosts the content seen through a shape. Both are uniform verbs on any layer, like opacity and blend.
ts
// Blur is a uniform layer effect, like opacity and blend. `node.blur(r)` softens
// a layer's own pixels; `node.backdropBlur(r)` frosts the content seen through a
// shape. Both are verbs on any layer.
composition(
{ width: 1280, height: 720, fps: 30, duration: "3s", background: "#0E1116" },
(comp) => {
const [cx, cy] = comp.center;
// Colorful content behind, so the frosted card has something to blur.
comp.add(ellipse({ size: 340, fill: "#7C5CFF" })).x(cx - 200).y(cy - 40);
comp.add(ellipse({ size: 280, fill: "#2BD4A8" })).x(cx + 220).y(cy + 70);
comp.add(rect({ size: 190, radius: 28, fill: "#FF6F4F" })).x(cx + 150).y(cy - 150).rotateTo(45);
// A soft glow: a self-blurred shape.
comp.add(ellipse({ size: 120, fill: "#FFD36E" })).x(cx).y(cy).blur(26);
// A frosted glass card over it all, blurring everything below it.
const card = comp
.add(rect({ size: [580, 210], radius: 28, fill: "rgba(14,17,22,0.45)" }))
.x(cx)
.y(cy)
.backdropBlur(26);
card.enter.pop({ from: 0.92 });
comp.add(text("Frosted", { size: 60, weight: 800, color: "#ffffff" })).x(cx).y(cy);
},
);Importing and animating an SVG
comp.svg(path) imports an SVG file as a group of vector shapes, centered on the canvas. Animate the whole drawing through the returned group, or reach a single shape by its SVG id with find (or by index with child) to move it, recolor it with fill and stroke, or draw it on. drawOn reveals a stroke from nothing to its full length. The drawing exports to Lottie as real vector shapes, and the draw-on exports as a native trim path. The file path is resolved relative to the composition script.
ts
composition({ width: 1280, height: 720, fps: 30, duration: "2.5s", background: "#0E1116" }, (comp) => {
// Import an SVG and animate it. The whole drawing scales up as one group;
// individual shapes are reached by their id to spin and to draw on.
const badge = comp.svg("badge.svg");
badge.scaleTo(3.4, { at: 0, duration: "0.8s" });
badge.find("ring").rotateTo(360, { at: 0, duration: "2s", ease: "easeInOut" });
badge.find("check").drawOn({ at: "0.7s", duration: "0.8s", ease: "easeInOut" });
});Imported SVG gradients
Linear and radial gradients in an imported SVG come through as real gradients, with multiple stops and per-stop opacity. They render in the preview and export to Lottie as gradient fills. The same gradients can be authored directly with gradient.linear and gradient.radial, taking a list of stops and geometry in fractions of the shape.
ts
composition({ width: 1280, height: 720, fps: 30, duration: "2.5s", background: "#0E1116" }, (comp) => {
// Imported SVG gradients: a radial glow and a linear bar. The whole drawing
// scales in, then the glow pulses, addressed by its id.
const art = comp.svg("svg-gradient.svg");
art.scaleTo(3, { at: 0, duration: "0.8s" });
art.find("sun").scaleTo(1.12, { at: "0.9s", duration: "0.7s" });
});Morphing one shape into another
morphTo animates a shape's outline into a target shape, given as another path layer or an SVG file. The two outlines are matched point to point and the target is fit into the source's box, so a circle smoothly becomes a star. The morph exports to Lottie as an animated path.
ts
composition({ width: 1280, height: 720, fps: 30, duration: "2.5s", background: "#0E1116" }, (comp) => {
// Morph one shape into another. The circle is a single-shape SVG, so the whole
// import morphs as a unit; the target is loaded once with loadSVG.
const shape = comp.svg("morph-circle.svg");
shape.scaleTo(3.4, { at: 0, duration: "0.7s" });
shape.morphTo(loadSVG("morph-star.svg"), { at: "0.8s", duration: "1s", ease: "inOut" });
});