Animate
One import for DOM animations, easing curves, springs, stagger and SVG. Install the TypeScript source, then describe where things move and how they get there.
Installation
pnpm dlx shadcn@latest add @latheui/animateThe registry adds lib/animate.ts and lib/curves.ts. It runs on the browser’s Web Animations API and needs no additional animation package.
import { animate } from "@/lib/animate"
const motion = animate(".target", {
to: { x: 220, rotate: 180 },
curve: { type: "spring", stiffness: 180, damping: 16 },
})Build a curve in the Animation Lab, then copy the generated call. Its preview, download and exported recipe share this implementation.
From and to
to describes the destination. Omit from to start at the element’s current computed values, or provide both endpoints for a reveal. Values are numbers: x/y, width, height and borderRadius use pixels; rotate uses degrees; scale and opacity are unitless. SVG strokeDashoffset is also supported.
import { animate } from "@/lib/animate"
animate(".letter", {
from: { y: 24, opacity: 0 },
to: { y: 0, opacity: 1 },
curve: { type: "bezier", points: [.22, 1, .36, 1] },
duration: 700,
stagger: 40,
})A target can be a CSS selector, one element, a NodeList or an array of elements. Transform channels are 2D and compose as translate, rotate, then scale. Cancel an earlier controller before starting another animation on the same properties.
Every curve, one option
Use a plain object in curve. Power, sine, expo, circ, back, elastic and bounce accept direction: in, out, inOut or outIn. Their default is out. The default curve is a power of 3.
// All curve types use the same option. No extra imports.
curve: "linear"
curve: { type: "linear", values: [0, .2, .8, 1] }
curve: { type: "power", power: 3, direction: "inOut" }
curve: { type: "sine", direction: "out" }
curve: { type: "expo", direction: "in" }
curve: { type: "circ", direction: "out" }
curve: { type: "back", overshoot: 1.7, direction: "out" }
curve: { type: "elastic", amplitude: 1, period: .3 }
curve: { type: "bounce", direction: "out" }
curve: { type: "bezier", points: [.22, 1, .36, 1] }
curve: { type: "steps", count: 6, jump: "end" }
curve: { type: "irregular", points: 12, randomness: .6, seed: 42 }
curve: { type: "spring", mass: 1, stiffness: 180, damping: 16, velocity: 0 }A spring calculates its own settling duration from its physics, with a minimum of 200 ms. Slow springs run until they settle. Other curves use duration, in milliseconds, defaulting to 600. Irregular curves use a seed so the same recipe produces the same motion.
Stagger and repeat
animate(".tile", {
to: { y: -24, scale: 1.5 },
curve: { type: "spring", stiffness: 180, damping: 16 },
delay: 100,
stagger: { each: 80, from: "center", grid: [7, 5] },
repeat: true,
alternate: true,
})A numeric stagger spaces targets in document order. The object form accepts first, center or last; grid is [columns, rows]. Centered grids use the distance from their center. Every target shares one cycle, so repeated staggered animations stay together.
repeat: true repeats indefinitely. A number sets additional cycles after the first; 0 plays once. alternate reverses every other cycle. Use repeatDelay for a pause between forward repeats; it defaults to 0 and is ignored with alternate.
Paths and polygons
animate(".traveler", {
follow: { path: "M 0 0 C 80 -60 160 60 240 0", rotate: true },
curve: "sine",
duration: 1200,
})
animate(".shape", {
polygon: {
from: [[50, 0], [100, 100], [0, 100]],
to: [[0, 0], [100, 0], [50, 100]],
},
duration: 700,
})Paths are sampled by distance, and rotate follows their tangent. Polygon vertices use percentages and both shapes need the same number of points. For custom effects, frame: (progress, index) => ({ ... }) can return any Web Animations keyframe; progress already includes the selected curve.
Playback and cleanup
motion.pause()
motion.seek(350) // Milliseconds within the shared cycle; pauses playback.
motion.play()
motion.reverse()
await motion.finished // Finite playback; cancellation rejects this promise.
motion.cancel() // Remove the animations and release listeners.duration on the returned controller is the full shared cycle, including stagger and repeat delay. animations exposes its native Animation objects. Set autoplay: false to create a paused controller. Reduced motion shows the final state and suppresses autoplay and repeat.
"use client"
import { useEffect, useRef } from "react"
import { animate } from "@/lib/animate"
export function Reveal() {
const target = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!target.current) return
const motion = animate(target.current, {
from: { y: 16, opacity: 0 },
to: { y: 0, opacity: 1 },
curve: { type: "spring", damping: 18 },
})
return () => motion.cancel()
}, [])
return <div ref={target}>Hello</div>
}Importing the module is safe on the server. Call animate() in a browser effect or event handler, after the elements exist.