Documentation menu
Guides
Animating elements
animate(target, values, options) moves elements to new values. This page covers what you can target, every kind of value it animates, and the tween and keyframe options.
Targets
The first argument picks the elements. A CSS selector animates every match. You can also pass one element, an array of elements or a NodeList.
import { animate } from "@animaly/dom";
animate(".card", { x: 120, opacity: 0.5 });import { animate } from "@animaly/dom";
const button = document.getElementById("save")!;
animate(button, { scale: 1.1 });import { animate } from "@animaly/dom";
const title = document.querySelector<HTMLElement>(".title")!;
const lede = document.querySelector<HTMLElement>(".lede")!;
animate([title, lede], { y: [12, 0], opacity: [0, 1] });import { animate } from "@animaly/dom";
const items = document.querySelectorAll<HTMLLIElement>("li");
animate(items, { x: [-20, 0], opacity: [0, 1] }, { duration: 0.4 });Transforms
x, y, z, rotate, rotateX, rotateY, skewX, skewY, scale, scaleX, scaleY and transformPerspective are animated as separate values and combined into one transform string per element, in the order translate, scale, rotate. That string replaces any transform the element already had.
import { animate } from "@animaly/dom";
// One transform string per element: translate, then scale, then rotate.
animate(".box", { x: 160, y: -40, scale: 1.2, rotate: 45 });import { animate } from "@animaly/dom";
animate(".box", { scaleX: [1, 1.3, 1], scaleY: [1, 0.7, 1] }, { duration: 0.5 });import { animate } from "@animaly/dom";
animate(".box", { skewX: 12, skewY: -4 });3D transforms
transformPerspective adds a perspective() in pixels to the same transform, so 3D rotations look deep without a perspective on the parent.
import { animate } from "@animaly/dom";
// transformPerspective adds perspective() to the transform, in px.
animate(".card-3d", { rotateY: 180, transformPerspective: 800 }, { duration: 0.6 });import { animate } from "@animaly/dom";
animate(".box", { rotateX: 20, z: -60, transformPerspective: 600 });Opacity and other CSS properties
Any CSS property animates. Use camelCase names. Plain numbers get px where CSS needs a unit; strings keep the unit you give them.
import { animate } from "@animaly/dom";
animate(".notice", { opacity: 0 }, { duration: 0.2 });import { animate } from "@animaly/dom";
// Plain numbers get px where CSS needs a unit.
animate(".box", { borderRadius: 40, letterSpacing: "0.1em", padding: 24 });Sizes, positions and auto
width, height, top, left, right and bottom convert between units by measuring the element, so you can go from pixels to percent, or to and from auto. The reads and writes for all elements of one call are batched together.
import { animate } from "@animaly/dom";
// 120px to 80% of the parent: animaly measures both and converts.
animate(".bar", { width: "80%" }, { duration: 0.6 });import { animate } from "@animaly/dom";
animate(".dot", { top: "50%", left: "75%" });Accordions with auto height
Animating height to "auto" measures the natural height of the content and animates to it.
import { animate } from "@animaly/dom";
const panel = document.querySelector<HTMLElement>(".panel")!;
await animate(panel, { height: "auto" }, { duration: 0.35, ease: "easeOut" });import { animate } from "@animaly/dom";
animate(".panel", { height: 0 }, { duration: 0.25, ease: "easeIn" });Colors
Colors can be hex, rgb(), rgba(), hsl() or hsla(), including colors inside other values like boxShadow. Named colors such as red are not parsed and throw an error.
import { animate } from "@animaly/dom";
// Colors are hex, rgb(), rgba(), hsl() or hsla().
animate(".button", { backgroundColor: "#7c3aed", color: "rgb(255, 255, 255)" });import { animate } from "@animaly/dom";
// hsl() and hsla() work too, with or without commas.
animate(".badge", { backgroundColor: "hsl(28 90% 52%)", color: "hsla(0, 0%, 100%, 0.9)" }, { duration: 0.6 });import { animate } from "@animaly/dom";
animate(".field", { borderColor: ["#d4d4d4", "#ec4899"] }, { duration: 0.2 });import { animate } from "@animaly/dom";
animate(".row", { backgroundColor: "rgba(253, 230, 138, 0)" }, { duration: 1.2 });CSS variables
Pass a custom property name in quotes. Its value can be a length, a number or a color.
import { animate } from "@animaly/dom";
animate(".grid", { "--gap": "24px" });import { animate } from "@animaly/dom";
animate(".theme", { "--accent": "#ec4899" }, { duration: 0.5 });Shadows, filters and clip paths
Values made of several numbers, like boxShadow, filter and clipPath, animate number by number, so give start and end the same list of functions. A start value of none becomes the zero version of the target.
import { animate } from "@animaly/dom";
// A start value of none becomes the zero version of the target.
animate(".card", { y: -4, boxShadow: "0 12px 24px rgba(0, 0, 0, 0.25)" });import { animate } from "@animaly/dom";
animate(".photo", { filter: ["blur(8px) brightness(0.6)", "blur(0px) brightness(1)"] }, { duration: 0.5 });import { animate } from "@animaly/dom";
animate(".reveal", { clipPath: "inset(0 0% 0 0)" }, { duration: 0.6, ease: "easeOut" });import { animate } from "@animaly/dom";
animate(".hero", { clipPath: "circle(75% at 50% 50%)" }, { duration: 0.8 });Keyframes
An array of values is a list of keyframes. By default they are spread evenly over the duration; times places each one at an offset between 0 and 1.
import { animate } from "@animaly/dom";
animate(".box", { x: [0, 120, 120, 0], rotate: [0, 0, 90, 90] }, { duration: 1.2 });import { animate } from "@animaly/dom";
// times are offsets from 0 to 1, one per keyframe.
animate(".box", { opacity: [0, 1, 1, 0] }, { duration: 2, times: [0, 0.1, 0.9, 1] });null keyframes
null as the first keyframe means the current value, so the animation starts from wherever the element is. A null later in the list holds the previous keyframe.
import { animate } from "@animaly/dom";
// null first: start wherever the box is now, overshoot, then settle.
animate(".box", { x: [null, 220, 200] }, { duration: 0.5 });import { animate } from "@animaly/dom";
// A later null holds the previous keyframe.
animate(".box", { scale: [1, 1.4, null, 1] }, { duration: 1 });Easing
ease takes a named ease (linear, easeIn, easeOut, easeInOut), a cubic bezier as four numbers, or a function from progress to progress. With keyframes, an array gives each segment its own ease.
import { animate } from "@animaly/dom";
animate(".box", { x: 200 }, { duration: 0.5, ease: "easeOut" });import { animate } from "@animaly/dom";
animate(".box", { x: 200 }, { duration: 0.6, ease: [0.22, 1, 0.36, 1] });import { animate } from "@animaly/dom";
const steps = (progress: number): number => Math.round(progress * 5) / 5;
animate(".box", { x: 200 }, { duration: 1, ease: steps });import { animate } from "@animaly/dom";
// One ease per segment: rise with easeOut, fall with easeIn.
animate(".box", { y: [0, -80, 0] }, { duration: 0.8, ease: ["easeOut", "easeIn"] });Delay and repeat
delay is in seconds. repeat is the number of extra iterations; use Infinity to loop until you stop it. repeatType decides how each iteration runs and repeatDelay pauses between them.
import { animate } from "@animaly/dom";
animate(".box", { opacity: [0, 1] }, { delay: 0.5 });import { animate } from "@animaly/dom";
animate(".box", { scale: [1, 1.15, 1] }, { duration: 0.6, repeat: 3 });import { animate } from "@animaly/dom";
animate(".spinner", { rotate: 360 }, { duration: 1, ease: "linear", repeat: Infinity });"loop"starts every iteration from the first keyframe."reverse"plays every other iteration backwards."mirror"swaps the keyframes every other iteration, so the ease still runs forwards.
import { animate } from "@animaly/dom";
// "reverse" plays every other iteration backwards.
animate(".box", { x: 200 }, { duration: 0.8, repeat: Infinity, repeatType: "reverse", repeatDelay: 0.2 });import { animate } from "@animaly/dom";
// "mirror" swaps the keyframes each iteration, so the ease runs forwards both ways.
animate(".box", { y: [0, -40] }, { duration: 0.5, ease: "easeOut", repeat: 5, repeatType: "mirror" });Tween and keyframe options
These options apply when type is omitted, "tween" or "keyframes".
| Option | Type | Default | What it does |
|---|---|---|---|
| duration | number | 0.3 | Length of one iteration in seconds. |
| delay | number | Stagger | 0 | Seconds to wait before starting. A stagger function gives each element its own delay. |
| ease | Ease | Ease[] | "easeInOut" | Named ease, cubic bezier, easing function, or one per keyframe segment. |
| times | number[] | none | Offset of each keyframe between 0 and 1. Evenly spaced when omitted. |
| repeat | number | 0 | Extra iterations after the first. Infinity loops until stopped. |
| repeatType | "loop" | "reverse" | "mirror" | "loop" | How each repeated iteration runs. |
| repeatDelay | number | 0 | Seconds between iterations. |
| onUpdate | (latest, name) => void | none | Called with each changed value and its property name, once per frame. |
| onComplete | () => void | none | Called when every value of the call has finished. |
Options apply to every property of the call. To give one property its own timing, use a sequence segment; see Stagger and timelines.
Callbacks and awaiting
onUpdate receives the latest value and the property name. onComplete runs once everything in the call has finished. The returned controls are also awaitable.
import { animate } from "@animaly/dom";
const readout = document.querySelector<HTMLOutputElement>(".readout")!;
animate(".box", { x: 200 }, {
duration: 1,
onUpdate: (latest, name) => {
readout.textContent = `${name}: ${latest}`;
},
});import { animate } from "@animaly/dom";
const toast = document.querySelector<HTMLElement>(".toast")!;
animate(toast, { opacity: 0 }, {
delay: 2,
onComplete: () => toast.remove(),
});import { animate } from "@animaly/dom";
const modal = document.querySelector<HTMLElement>(".modal")!;
await animate(modal, { opacity: 0, scale: 0.95 }, { duration: 0.18 });
modal.remove();Stopping and interrupting
stop() leaves the values where they are. Starting a new animation on the same property takes over from its current value, so you rarely need to stop first.
import { animate } from "@animaly/dom";
const controls = animate(".box", { x: 400 }, { duration: 2 });
setTimeout(() => controls.stop(), 500);import { animate } from "@animaly/dom";
animate(".box", { x: 400 }, { duration: 2 });
// The second call takes over x from its current value.
setTimeout(() => animate(".box", { x: 0 }, { duration: 0.4 }), 600);canAnimate(element, name) tells you whether animaly can animate a property on an element before you call it.
import { animate, canAnimate } from "@animaly/dom";
const box = document.querySelector<HTMLElement>(".box")!;
if (canAnimate(box, "borderRadius")) {
animate(box, { borderRadius: 40 });
}