Skip to main content
Rezi provides declarative animation hooks for smooth transitions, spring physics, and complex sequences.

Animation Hooks

All animation hooks are available in defineWidget contexts:
import { defineWidget, useTransition, useSpring } from "@rezi-ui/core";

const AnimatedWidget = defineWidget((props, ctx) => {
  // Animation hooks go here
  const opacity = useTransition(ctx, props.visible ? 1 : 0);
  
  return ui.box({ opacity: opacity.value }, children);
});

Transition Animations

Smooth eased transitions between values:
import { defineWidget, useTransition, ui } from "@rezi-ui/core";

const FadeInOut = defineWidget<{ visible: boolean }>((props, ctx) => {
  const opacity = useTransition(ctx, props.visible ? 1 : 0, {
    duration: 200,    // milliseconds
    easing: "easeOut",
  });
  
  return ui.box({ opacity: opacity.value }, [
    ui.text("Fading content"),
  ]);
});

Configuration

type UseTransitionConfig = {
  duration?: number;           // Animation duration in ms (default: 160)
  easing?: EasingInput;        // Easing function
  delay?: number;              // Delay before start (default: 0)
  onComplete?: () => void;     // Called when animation finishes
};

Easing Functions

useTransition(ctx, target, { easing: "linear" })
useTransition(ctx, target, { easing: "easeIn" })
useTransition(ctx, target, { easing: "easeOut" })
useTransition(ctx, target, { easing: "easeInOut" })

Spring Animations

Physics-based spring animations:
import { useSpring } from "@rezi-ui/core";

const BouncyBox = defineWidget<{ x: number }>((props, ctx) => {
  const pos = useSpring(ctx, props.x, {
    stiffness: 200,   // Spring stiffness (default: 180)
    damping: 20,      // Damping ratio (default: 12)
    mass: 1,          // Mass (default: 1)
  });
  
  return ui.box({
    position: "absolute",
    left: Math.round(pos.value),
    border: "single",
  }, [ui.text("Spring!")]);
});

Spring Configuration

type UseSpringConfig = {
  stiffness?: number;      // Spring stiffness (higher = faster)
  damping?: number;        // Damping (higher = less oscillation)
  mass?: number;           // Mass (higher = slower)
  precision?: number;      // Rest threshold (default: 0.01)
  onComplete?: () => void; // Called when spring settles
};

Common Presets

// Bouncy
useSpring(ctx, target, { stiffness: 200, damping: 10 })

// Smooth
useSpring(ctx, target, { stiffness: 100, damping: 20 })

// Stiff
useSpring(ctx, target, { stiffness: 300, damping: 30 })

Sequence Animations

Chain multiple keyframes:
import { useSequence } from "@rezi-ui/core";

const Pulse = defineWidget<{ active: boolean }>((props, ctx) => {
  const scale = useSequence(ctx, {
    keyframes: [
      { value: 1, duration: 0 },
      { value: 1.2, duration: 150, easing: "easeOut" },
      { value: 1, duration: 150, easing: "easeIn" },
    ],
    loop: props.active,
  });
  
  return ui.box({ w: Math.round(20 * scale.value) }, [
    ui.text("Pulsing"),
  ]);
});

Sequence Options

type UseSequenceConfig = {
  keyframes: Array<{
    value: number;
    duration: number;      // Duration to reach this value
    easing?: EasingInput;
  }>;
  loop?: boolean;          // Loop the sequence
  delay?: number;          // Initial delay
  onComplete?: () => void;
};

Stagger Animations

Animate multiple items with delays:
import { useStagger, each } from "@rezi-ui/core";

const StaggeredList = defineWidget<{ items: string[] }>((props, ctx) => {
  const stagger = useStagger(ctx, props.items.length, {
    staggerMs: 50,      // Delay between items
    duration: 200,
    easing: "easeOut",
  });
  
  return ui.column({ gap: 1 }, 
    each(props.items, (item, index) => 
      ui.box({ opacity: stagger[index]?.value || 0 }, [
        ui.text(item),
      ])
    )
  );
});

Parallel Animations

Animate multiple properties simultaneously:
import { useParallel } from "@rezi-ui/core";

const SlideAndFade = defineWidget<{ visible: boolean }>((props, ctx) => {
  const animations = useParallel(ctx, [
    { target: props.visible ? 0 : 20, config: { duration: 200 } },  // x offset
    { target: props.visible ? 1 : 0, config: { duration: 200 } },   // opacity
  ]);
  
  return ui.box({
    position: "absolute",
    left: Math.round(animations[0].value),
    opacity: animations[1].value,
  }, [ui.text("Slide and fade")]);
});

Chained Animations

Run animations in sequence:
import { useChain } from "@rezi-ui/core";

const SequentialMove = defineWidget<{ stage: number }>((props, ctx) => {
  const pos = useChain(ctx, [
    { target: 0, config: { duration: 200 } },   // Start at 0
    { target: 20, config: { duration: 200 } },  // Move to 20
    { target: 10, config: { duration: 200 } },  // Move to 10
  ][props.stage] || { target: 0 });
  
  return ui.box({
    position: "absolute",
    left: Math.round(pos.value),
    border: "single",
  }, [ui.text("Moving")]);
});

Animated Value

Low-level animated value with playback control:
import { useAnimatedValue } from "@rezi-ui/core";

const CustomAnimation = defineWidget((props, ctx) => {
  const animated = useAnimatedValue(ctx, 0, {
    mode: "transition",
    transition: { duration: 300, easing: "easeInOut" },
  });
  
  // Control playback
  animated.start(100);     // Animate to 100
  animated.stop();         // Stop animation
  animated.set(50);        // Jump to value
  
  return ui.box({ w: Math.round(animated.value) }, [
    ui.text("Animated width"),
  ]);
});

Container Transitions

Declarative transitions on containers:
ui.box({
  transition: {
    duration: 200,
    easing: "easeOut",
    properties: ["position", "size"],  // Animate only these properties
  },
  border: "single",
}, children)
Container transitions automatically animate:
  • Position (x, y) changes
  • Size (w, h) changes
  • Opacity changes

Exit Transitions

Animate widgets before unmount:
ui.box({
  exitTransition: {
    duration: 200,
    easing: "easeIn",
  },
  opacity: state.visible ? 1 : 0,
}, children)
When removed from the tree, the widget fades out over 200ms before being destroyed.

Real-World Examples

Slide-in Sidebar

const Sidebar = defineWidget<{ open: boolean }>((props, ctx) => {
  const width = useTransition(ctx, props.open ? 30 : 0, {
    duration: 250,
    easing: "easeOut",
  });
  
  return ui.box({
    w: Math.round(width.value),
    h: "100%",
    overflow: "hidden",
    border: "single",
  }, [
    ui.column({ gap: 1, p: 1 }, [
      ui.text("Sidebar", { variant: "heading" }),
      ui.text("Content..."),
    ]),
  ]);
});

Loading Spinner

const Spinner = defineWidget((props, ctx) => {
  const frame = useSequence(ctx, {
    keyframes: [
      { value: 0, duration: 0 },
      { value: 1, duration: 100 },
      { value: 2, duration: 100 },
      { value: 3, duration: 100 },
    ],
    loop: true,
  });
  
  const frames = ["⠋", "⠙", "⠹", "⠸"];
  const current = frames[Math.floor(frame.value) % frames.length];
  
  return ui.text(current, { style: { fg: "accent.primary" } });
});

Notification Toast

const Toast = defineWidget<{ message: string; visible: boolean }>((props, ctx) => {
  const opacity = useTransition(ctx, props.visible ? 1 : 0, {
    duration: 200,
    easing: "easeOut",
  });
  
  const y = useSpring(ctx, props.visible ? 0 : -5, {
    stiffness: 200,
    damping: 20,
  });
  
  return ui.box({
    position: "absolute",
    top: Math.round(y.value),
    right: 1,
    opacity: opacity.value,
    preset: "elevated",
    shadow: true,
  }, [
    ui.text(props.message),
  ]);
});

Performance

Animation hooks use requestAnimationFrame-equivalent timing:
  • Frame rate: 60fps (16.67ms per frame)
  • Overhead: Minimal - only animating widgets re-render
  • Batching: Multiple animations batch into single render
From benchmarks:
  • Simple transitions: ~0.1ms overhead per frame
  • Complex sequences: ~0.5ms overhead per frame
Animations automatically pause when the terminal is not visible or the app is idle. This prevents wasted CPU cycles.

Best Practices

Use Transitions

Prefer useTransition for simple value animations. It’s the most straightforward and predictable.

Springs for Physics

Use useSpring when you want natural, physics-based motion with overshoot and settling.

Container Transitions

Use declarative transition props on containers for automatic position/size animations without hooks.

Exit Transitions

Always use exitTransition for removing items from lists. Instant disappearance feels jarring.

Next Steps

Routing

Implement page navigation with animated transitions

Graphics

Draw charts, images, and custom graphics

Build docs developers (and LLMs) love