Skip to content

Motion

All motion APIs work in both modes: in DOM mode the same numbers become CSS transforms, and in GPU mode they only update a matrix. Durations are in milliseconds. With prefers-reduced-motion, motion jumps to its end state.

const gpu = useGPUHost();
const handle = gpu.transition("card", {
keyframes: [{ offset: 0 }, { offset: 1, x: 120, rotation: 10, opacity: 0.8 }],
timing: { duration: 500, easing: "ease-out" },
});
const result = await handle.finished; // { status: "completed" | "cancelled" | ..., reason }
  • Keyframes are numeric: x, y, rotation (degrees), scaleX, scaleY and opacity. An omitted channel means its identity value, not “keep the previous one”.
  • Layers can also tilt out of the screen: tiltX and tiltY in degrees (at most ±60, as CSS rotateX and rotateY), viewed from perspective CSS px (1600 by default). Clicks follow the tilted controls exactly. A tilted layer must not sit inside a scrolling box that clips it, or clip its own overflow.
  • Starting a new transition on the same layer continues smoothly from where the current one is.
  • Rotation and scale pivot around the layer’s origin. Set origin="center" on the Surface (or pass origin to useSurfaceRef) for natural spins.
  • While a transition runs, the layer’s native controls stay clickable at their moving positions.
gpu.transitionGroup(cards.map(card => ({ id: card.id, motion: dealTo(card) })), 90);

All items start together, offset by the stagger (0–1000 ms), and each has its own handle. Invalid input fails as a whole, before anything moves.

<Surface id="toast" layout={...}>
<Presence id="toast" present={open} enter={slideIn} exit={slideOut}
onComplete={result => log(result)}>
<Toast onDismiss={() => setOpen(false)} />
</Presence>
</Surface>

Presence keeps its children mounted through the exit motion, then removes them. The Surface stays as a stable layer. The toast’s buttons are the real React buttons the whole time.

  • controller.animate(id, target, { duration }) interpolates toward a target transform on the GPU. It is meant for interactive, imperative motion.
  • controller.setTransform(id, transform) sets a transform immediately. It is safe to call every frame (drag, springs, physics), including while enable() is admitting; admission presents the latest value.
  • A Surface’s transform prop is the resting transform. React updates to it during a transition become the end state when that transition finishes.

Animation ownership (existing CSS or WAAPI animations)

Section titled “Animation ownership (existing CSS or WAAPI animations)”

If a layer already has a CSS keyframe animation or a Web Animation of transform or opacity, GPU mode needs to own it, because the GPU draws the layer’s transform itself.

  • controller.createMotion(id, descriptor) creates a managed animation from numeric keyframes. Call .own() to hand it to the GPU. play, pause and seek work either way.
  • controller.ownAnimation(id, animation) takes over an existing Web Animation; ownCSSAnimation(id, { rule, keyframes }) takes over a CSS animation whose rules you point to.
  • playback.release() hands the source back and returns the surface to DOM. playback.release({ freeze: true }) releases only that layer and keeps GPU on: the layer stays at its last pose (WAAPI and createMotion sources).

Ownership works while GPU is already on. For new code, prefer transition and Presence; ownership exists for animations you cannot rewrite.

Transitions pause while the document is hidden and resume with the same progress. They never jump to the end.