animaly
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.

Animate every element that matches a selector
import { animate } from "@animaly/dom";

animate(".card", { x: 120, opacity: 0.5 });
Animate one element
import { animate } from "@animaly/dom";

const button = document.getElementById("save")!;

animate(button, { scale: 1.1 });
Animate an array of elements
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] });
Animate a NodeList
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.

Move, scale and rotate together
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 });
Squash and stretch with scaleX and scaleY
import { animate } from "@animaly/dom";

animate(".box", { scaleX: [1, 1.3, 1], scaleY: [1, 0.7, 1] }, { duration: 0.5 });
Skew an element
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.

Flip a card in 3D
import { animate } from "@animaly/dom";

// transformPerspective adds perspective() to the transform, in px.
animate(".card-3d", { rotateY: 180, transformPerspective: 800 }, { duration: 0.6 });
Tilt and push back along z
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.

Fade an element out
import { animate } from "@animaly/dom";

animate(".notice", { opacity: 0 }, { duration: 0.2 });
Animate any CSS property
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.

Animate width between units
import { animate } from "@animaly/dom";

// 120px to 80% of the parent: animaly measures both and converts.
animate(".bar", { width: "80%" }, { duration: 0.6 });
Animate top and left
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.

Open an accordion to its natural height
import { animate } from "@animaly/dom";

const panel = document.querySelector<HTMLElement>(".panel")!;

await animate(panel, { height: "auto" }, { duration: 0.35, ease: "easeOut" });
Close an accordion from auto to 0
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.

Animate background and text color
import { animate } from "@animaly/dom";

// Colors are hex, rgb(), rgba(), hsl() or hsla().
animate(".button", { backgroundColor: "#7c3aed", color: "rgb(255, 255, 255)" });
Shift a badge's hue with hsl()
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 });
Highlight a field's border
import { animate } from "@animaly/dom";

animate(".field", { borderColor: ["#d4d4d4", "#ec4899"] }, { duration: 0.2 });
Fade a background to transparent
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.

Animate a CSS variable
import { animate } from "@animaly/dom";

animate(".grid", { "--gap": "24px" });
Animate a theme color variable
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.

Lift a card with a shadow
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)" });
Blur and brighten with filter
import { animate } from "@animaly/dom";

animate(".photo", { filter: ["blur(8px) brightness(0.6)", "blur(0px) brightness(1)"] }, { duration: 0.5 });
Reveal with clip-path inset
import { animate } from "@animaly/dom";

animate(".reveal", { clipPath: "inset(0 0% 0 0)" }, { duration: 0.6, ease: "easeOut" });
Grow a circular reveal
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.

Run through keyframes
import { animate } from "@animaly/dom";

animate(".box", { x: [0, 120, 120, 0], rotate: [0, 0, 90, 90] }, { duration: 1.2 });
Place keyframes with times
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.

Start from the current value with null
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 });
Hold a value with null
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.

Use a named ease
import { animate } from "@animaly/dom";

animate(".box", { x: 200 }, { duration: 0.5, ease: "easeOut" });
Use a cubic bezier
import { animate } from "@animaly/dom";

animate(".box", { x: 200 }, { duration: 0.6, ease: [0.22, 1, 0.36, 1] });
Use an easing function
import { animate } from "@animaly/dom";

const steps = (progress: number): number => Math.round(progress * 5) / 5;

animate(".box", { x: 200 }, { duration: 1, ease: steps });
Ease each keyframe segment differently
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.

Delay the start
import { animate } from "@animaly/dom";

animate(".box", { opacity: [0, 1] }, { delay: 0.5 });
Repeat a pulse three more times
import { animate } from "@animaly/dom";

animate(".box", { scale: [1, 1.15, 1] }, { duration: 0.6, repeat: 3 });
Loop forever
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.
Go back and forth with repeatType
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 });
Mirror an eased animation
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".

Tween and keyframe options
OptionTypeDefaultWhat it does
durationnumber0.3Length of one iteration in seconds.
delaynumber | Stagger0Seconds to wait before starting. A stagger function gives each element its own delay.
easeEase | Ease[]"easeInOut"Named ease, cubic bezier, easing function, or one per keyframe segment.
timesnumber[]noneOffset of each keyframe between 0 and 1. Evenly spaced when omitted.
repeatnumber0Extra iterations after the first. Infinity loops until stopped.
repeatType"loop" | "reverse" | "mirror""loop"How each repeated iteration runs.
repeatDelaynumber0Seconds between iterations.
onUpdate(latest, name) => voidnoneCalled with each changed value and its property name, once per frame.
onComplete() => voidnoneCalled 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.

Read values on every frame
import { animate } from "@animaly/dom";

const readout = document.querySelector<HTMLOutputElement>(".readout")!;

animate(".box", { x: 200 }, {
  duration: 1,
  onUpdate: (latest, name) => {
    readout.textContent = `${name}: ${latest}`;
  },
});
Run code when it finishes
import { animate } from "@animaly/dom";

const toast = document.querySelector<HTMLElement>(".toast")!;

animate(toast, { opacity: 0 }, {
  delay: 2,
  onComplete: () => toast.remove(),
});
Await an exit before removing an element
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.

Stop an animation where it is
import { animate } from "@animaly/dom";

const controls = animate(".box", { x: 400 }, { duration: 2 });

setTimeout(() => controls.stop(), 500);
Start a new animation over a running one
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.

Check whether a property can animate
import { animate, canAnimate } from "@animaly/dom";

const box = document.querySelector<HTMLElement>(".box")!;

if (canAnimate(box, "borderRadius")) {
  animate(box, { borderRadius: 40 });
}