Motion

Motion animates a chart as it first draws and as its data changes, so readers can follow each value to its new place.

LLMs

Code

Animations work the same way on every TanStack chart, through the kit's Chart components. The options below go into the chart's TanStack definition, written the way TanStack documents it.

Install

npx antuikit@latest init
npx antuikit@latest add tanstack-chart
The first command installs AntuikitProvider and the theme tokens. The second adds the chart components and TanStack Charts itself.

Which component

All three take the same definition and the same palette props.
Chart components

Turn motion on

Animation comes from the motion() renderer. Pass it to RendererChart, which takes the same definition and props as Chart. Create the renderer once, outside the component or in state.
import { motion } from "@tanstack/charts/motion"
import { RendererChart } from "@/components/ui/tanstack-chart"
const renderer = motion({
transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
})
<RendererChart
definition={definition}
renderer={renderer}
ariaLabel="Monthly revenue against target"
height={220}
/>
With no options, every change is a 1,100 millisecond tween. Bars, lines and areas grow from their baseline on the first paint, and removed items stay on screen until they have animated out.

Tweens and springs

A tween runs for a set duration along an easing curve. A spring has no duration: it settles by stiffness, damping and mass, and when an update interrupts it, it carries its speed into the new target. Set the transition for the whole chart with motion in the definition.
defineChart({
motion: {
transition: spring
? { type: "spring", stiffness: 170, damping: 18, mass: 1 }
: { type: "tween", duration: 520, easing: "ease-in-out" },
},
marks,
scales,
})
Use a spring for changes the reader triggers again and again. Use a tween for a one-time entrance. Lower damping makes a spring overshoot, which shows values the data never had.

Timing per mark and per item

Every mark takes a motion option too. Pass an object, or a function that gets a context with the phase (enter, update or exit), the datumIndex and the datum.
barY(months, {
x: "period",
y: "actual",
key: "id",
motion(context) {
if (context.datum?.featured) {
return { delay: 200, transition: { type: "tween", duration: 700 } }
}
return { delay: context.datumIndex * 55 }
},
}),
lineY(months, {
x: "period",
y: "target",
key: "id",
motion: { delay: 110, transition: { type: "tween", duration: 860 } },
}),
  • Settings cascade from the renderer to the chart, the mark and the axis. A partial transition of the same type keeps the fields it leaves out.
  • A delay you set replaces the automatic stagger. A spring ignores delays on updates, so it never stalls mid-flight.
  • stagger({ each: 35 }) from @tanstack/charts/motion returns the same per-item delay, to spread into a motion object.
  • motion: false turns motion off for one scope.

Stable keys

The renderer matches items between two states by key. Give every row a key that belongs to the item, not to its position, so a sorted, added or removed item moves, enters or leaves instead of morphing into its neighbor.
barY(rows, { x: "period", y: "actual", key: "id" })
// Several series in one mark: make the key unique across them.
barY(rows, { x: "month", y: "value", z: "series", key: (row) => `${row.month}:${row.series}` })
Without a key, a mark uses datum.id or its category, and falls back to the array position when those repeat.

Axes

An axis, its ticks, its tick labels and its label each take a motion option. Grid lines move with their axis.
const guideMotion = {
transition: { type: "tween", duration: 260, easing: "ease-out" },
}
scales: {
x: {
scale: scaleBand().domain(rows.map((row) => row.period)),
axis: {
motion: guideMotion,
ticks: { motion: guideMotion },
tickLabels: {
motion: (context) => ({
delay: context.datumIndex * 18,
transition: { type: "tween", duration: 220 },
}),
},
},
},
}

Focus, crosshair and tooltip

A focus states entry takes its own transition, and so does a crosshair, so hovered points and the cursor glide from one point to the next.
const focusSpring = { type: "spring", stiffness: 240, damping: 22, mass: 0.72 }
dot(rows, {
x: "day",
y: "value",
z: "channel",
key: "id",
states: [
{ when: { focus: "unmatched" }, style: { opacity: 0.14 }, transition: focusSpring },
{ when: { focus: "primary" }, style: { r: 8 }, transition: focusSpring },
],
}),
crosshair({
motion: { transition: { type: "spring", stiffness: 320, damping: 28 } },
}),
  • states wait until an entrance or a data update has finished. For a focus mark that must show during one, use a separate whenFocused mark.
  • The tooltip uses the renderer's transition. Pass motion: false in the tooltip options to keep it immediate while the data animates.

First paint and replay

initial controls the entrance. true, the default, animates the first paint in the browser. false draws it at once and animates only later changes. "always" also replays the entrance over a chart rendered on the server. Updates caused only by a resize do not animate unless you set resize: true.
To replay an entrance, give the chart a new renderer and a new key:
const [renderer, setRenderer] = useState(() => motion())
const [replays, setReplays] = useState(0)
const replay = () => {
setReplays((value) => value + 1)
setRenderer(motion())
}
<RendererChart key={replays} renderer={renderer} definition={definition} ariaLabel="..." />

Reduced motion

motion() draws the final state at once when the reader prefers reduced motion. That covers entrances, updates, focus states and the tooltip. Leave respectReducedMotion at its default of true.

A lighter tween

For a short tween on Chart, without the renderer, set svgAnimation in the definition. It has no springs, no per-mark timing and no staged entrance, and it also respects reduced motion.
defineChart({
svgAnimation: { duration: 280, easing: "ease-out" },
marks,
scales,
})

API

The renderer's options, the motion option every scope takes, and the two transitions. TanStack Charts' documentation has the full reference.
motion()
motion option
Transitions