Skip to main content

Overview

Animation hooks provide declarative animation primitives for smooth UI transitions. All hooks run at 60 FPS and support playback controls.
Animation hooks must be called in defineWidget contexts only, following standard hook rules.

useTransition

Animate from current value to target value over time with easing.
ctx
WidgetContext
required
Widget context from defineWidget.
value
number
required
Target numeric value to animate to.
config
UseTransitionConfig
Transition configuration.
config.duration
number
Duration in milliseconds (default: 160ms).
config.delay
number
Delay before animation starts (default: 0ms).
config.easing
string | (t: number) => number
Easing function. Built-in: "linear", "ease-in", "ease-out", "ease-in-out" (default).
config.playback
PlaybackControl
Playback controls (paused, reversed, rate).
config.onComplete
() => void
Callback fired when animation completes.
Returns: Current interpolated number
import { defineWidget, useTransition, ui } from "@rezi-ui/core";

const FadeIn = defineWidget<{ visible: boolean }>((props, ctx) => {
  const opacity = useTransition(ctx, props.visible ? 1 : 0, {
    duration: 300,
    easing: "ease-in-out",
  });

  return ui.box({
    opacity,
    children: [ui.text("Fading content")],
  });
});

Easing Functions

import { useTransition, ui } from "@rezi-ui/core";

const Widget = defineWidget((props, ctx) => {
  const linear = useTransition(ctx, props.value, { easing: "linear" });
  const easeIn = useTransition(ctx, props.value, { easing: "ease-in" });
  const easeOut = useTransition(ctx, props.value, { easing: "ease-out" });
  const easeInOut = useTransition(ctx, props.value, { easing: "ease-in-out" });

  return ui.box({ x: linear });
});

Playback Control

import { defineWidget, useTransition, ui } from "@rezi-ui/core";

const ControlledAnimation = defineWidget((props, ctx) => {
  const [paused, setPaused] = ctx.useState(false);
  const [reversed, setReversed] = ctx.useState(false);
  const [rate, setRate] = ctx.useState(1);

  const x = useTransition(ctx, 100, {
    duration: 2000,
    playback: { paused, reversed, rate },
  });

  return ui.column([
    ui.box({ x, w: 10, h: 3 }, [ui.text("Moving")]),
    ui.button({ id: ctx.id("pause"), label: paused ? "Resume" : "Pause", onPress: () => setPaused(!paused) }),
    ui.button({ id: ctx.id("reverse"), label: "Reverse", onPress: () => setReversed(!reversed) }),
    ui.button({ id: ctx.id("speed"), label: `Speed: ${rate}x`, onPress: () => setRate(rate === 1 ? 2 : 1) }),
  ]);
});

useSpring

Physics-based spring animation with mass, stiffness, and damping.
ctx
WidgetContext
required
Widget context.
value
number
required
Target value.
config
UseSpringConfig
Spring configuration.
config.mass
number
Mass of the spring (default: 1).
config.stiffness
number
Spring stiffness (default: 170).
config.damping
number
Damping ratio (default: 26).
config.velocity
number
Initial velocity (default: 0).
config.restThreshold
number
Velocity threshold for “at rest” (default: 0.01).
Returns: AnimatedValue with { value, velocity, isAnimating }
import { defineWidget, useSpring, ui } from "@rezi-ui/core";

const SpringBox = defineWidget<{ x: number }>((props, ctx) => {
  const spring = useSpring(ctx, props.x, {
    stiffness: 200,
    damping: 20,
    mass: 1,
  });

  return ui.column([
    ui.box({ x: spring.value, w: 10, h: 3 }, [ui.text("Springy")]),
    ui.text(`Velocity: ${spring.velocity.toFixed(2)}`),
    ui.text(spring.isAnimating ? "Animating" : "At rest"),
  ]);
});

Spring Presets

// Bouncy spring
const bouncy = useSpring(ctx, value, {
  stiffness: 300,
  damping: 10,
});

// Gentle spring
const gentle = useSpring(ctx, value, {
  stiffness: 120,
  damping: 20,
});

// Stiff spring
const stiff = useSpring(ctx, value, {
  stiffness: 400,
  damping: 30,
});

// Slow spring
const slow = useSpring(ctx, value, {
  stiffness: 80,
  damping: 15,
  mass: 2,
});

useSequence

Keyframe-based animation sequence.
ctx
WidgetContext
required
Widget context.
config
UseSequenceConfig
required
Sequence configuration.
config.keyframes
SequenceKeyframe[]
required
Array of keyframes with time and value.
config.duration
number
Total duration in milliseconds.
config.loop
boolean
Loop animation (default: false).
config.autoplay
boolean
Start automatically (default: true).
Returns: { value: number, progress: number, isAnimating: boolean }
import { defineWidget, useSequence, ui } from "@rezi-ui/core";

const Pulse = defineWidget((props, ctx) => {
  const { value: scale } = useSequence(ctx, {
    keyframes: [
      { time: 0, value: 1 },
      { time: 0.5, value: 1.2 },
      { time: 1, value: 1 },
    ],
    duration: 1000,
    loop: true,
  });

  return ui.box({
    w: Math.round(20 * scale),
    h: Math.round(5 * scale),
  }, [ui.text("Pulsing")]);
});

Complex Sequences

const ComplexAnimation = defineWidget((props, ctx) => {
  const { value: opacity, progress } = useSequence(ctx, {
    keyframes: [
      { time: 0, value: 0, easing: "ease-in" },
      { time: 0.3, value: 1, easing: "linear" },
      { time: 0.7, value: 1, easing: "ease-out" },
      { time: 1, value: 0 },
    ],
    duration: 2000,
    onComplete: () => console.log("Sequence done"),
  });

  return ui.column([
    ui.box({ opacity }, [ui.text("Fading")]),
    ui.progress({ value: progress, max: 1 }),
  ]);
});

useStagger

Stagger animations across multiple items with delay offset.
ctx
WidgetContext
required
Widget context.
config
UseStaggerConfig
required
Stagger configuration.
config.count
number
required
Number of items to stagger.
config.stagger
number
Delay between each item in milliseconds (default: 50ms).
config.duration
number
Duration per item (default: 160ms).
config.from
'start' | 'end' | 'center'
Stagger direction (default: ‘start’).
Returns: number[] - Array of animated values for each item
import { defineWidget, useStagger, ui } from "@rezi-ui/core";

const StaggeredList = defineWidget<{ items: string[]; visible: boolean }>((props, ctx) => {
  const opacities = useStagger(ctx, {
    count: props.items.length,
    target: props.visible ? 1 : 0,
    stagger: 50,
    duration: 300,
  });

  return ui.column(
    props.items.map((item, i) =>
      ui.box({ opacity: opacities[i] }, [ui.text(item)])
    )
  );
});

Stagger Directions

// Stagger from start (0, 1, 2, ...)
const fromStart = useStagger(ctx, {
  count: 5,
  target: 1,
  from: "start",
  stagger: 100,
});

// Stagger from end (4, 3, 2, 1, 0)
const fromEnd = useStagger(ctx, {
  count: 5,
  target: 1,
  from: "end",
  stagger: 100,
});

// Stagger from center (2, 1&3, 0&4)
const fromCenter = useStagger(ctx, {
  count: 5,
  target: 1,
  from: "center",
  stagger: 100,
});

useAnimatedValue

Low-level animated value with manual control over transition/spring mode.
ctx
WidgetContext
required
Widget context.
target
number
required
Target value.
config
UseAnimatedValueConfig
required
Configuration with mode selection.
Returns: AnimatedValue
import { defineWidget, useAnimatedValue, ui } from "@rezi-ui/core";

const CustomAnimation = defineWidget((props, ctx) => {
  const animated = useAnimatedValue(ctx, props.target, {
    mode: "transition",
    transition: {
      duration: 500,
      easing: "ease-in-out",
    },
  });

  return ui.box({ x: animated.value });
});

useParallel

Run multiple animations in parallel with different configs.
ctx
WidgetContext
required
Widget context.
configs
UseParallelConfig
required
Array of animation configs.
Returns: ParallelAnimationEntry[]
import { defineWidget, useParallel, ui } from "@rezi-ui/core";

const ParallelDemo = defineWidget<{ visible: boolean }>((props, ctx) => {
  const [x, y, opacity] = useParallel(ctx, [
    { target: props.visible ? 100 : 0, config: { duration: 500 } },
    { target: props.visible ? 50 : 0, config: { duration: 700 } },
    { target: props.visible ? 1 : 0, config: { duration: 300 } },
  ]);

  return ui.box({
    x: x.value,
    y: y.value,
    opacity: opacity.value,
  }, [ui.text("Moving")]);
});

useChain

Chain animations sequentially (one after another).
ctx
WidgetContext
required
Widget context.
configs
UseChainConfig
required
Array of animation configs (executed in order).
Returns: number - Current animated value
import { defineWidget, useChain, ui } from "@rezi-ui/core";

const ChainDemo = defineWidget((props, ctx) => {
  const x = useChain(ctx, [
    { target: 50, config: { duration: 500 } },
    { target: 100, config: { duration: 500 } },
    { target: 0, config: { duration: 500 } },
  ]);

  return ui.box({ x }, [ui.text("Chaining")]);
});

Container Transitions

Widgets like ui.box, ui.column, ui.row, and ui.grid support automatic transitions:
import { ui } from "@rezi-ui/core";

ui.box({
  transition: {
    duration: 300,
    properties: ["position", "size", "opacity"],
  },
  exitTransition: {
    duration: 200,
    properties: ["opacity"],
  },
}, [/* children */]);

Performance Tips

Animating 100+ values simultaneously can impact frame rate. Use useStagger for large lists.
useTransition is lighter than useSpring when you don’t need physics.
When widgets are off-screen or inactive, pause animations to save CPU.
const x = useTransition(ctx, target, {
  playback: { paused: !props.visible },
});
For layout properties (x, y, width, height), round to integers:
const x = Math.round(useTransition(ctx, target));

defineWidget

Widget composition API

State Hooks

useState and useRef

Animation Guide

Animation patterns and examples

Animation Lab

Interactive animation showcase

Build docs developers (and LLMs) love