Skip to main content
Version: Next

Animations

Ribir provides a multi-layer animation system centered around the @Animate primitive. It ranges from low-level property interpolation to high-level state orchestration.

Basic Concept: @Animate

@Animate drives a value over time using three core properties:

  • state: The target writer (e.g., widget.margin(), widget.opacity()).
  • from: The starting value.
  • transition: The timing function (e.g., EasingTransition).

Runtime Behavior

  1. Per-frame interpolation: Calculating and writing values to the state during rendering.
  2. Post-render restoration: Discarding temporary values after painting to keep the data model pure.
  3. Shallow updates: Using shallow() writes to trigger repaints without redundant pipe! recalculations.

⚠️ IMPORTANT: Bind @Animate directly to a Widget's property Writer. Binding to an isolated Stateful variable will fail to trigger UI updates due to shallow write isolation.

// ✅ CORRECT: Animate Widget's state directly
fn_widget! {
let w = @Container { opacity: 1. };
let animate = @Animate {
state: w.opacity(),
from: 0.,
transition: EasingTransition {
duration: Duration::from_millis(300),
easing: easing::LINEAR
}
};
@(w) { on_tap: move |_| animate.run() }
}

Animation Toolkit

Auto-animate with transition()

Attach a transition directly to a StateWriter to animate every value change automatically.

  • transition(...): Animates from the current value.
  • transition_with_init(init, ...): Useful for entrance animations starting from a custom value (e.g., 0.0 opacity).
use ribir::prelude::*;

fn demo() -> Widget<'static> {
fn_widget! {
let mut w = @Container { size: Size::new(40., 20.) };
w.opacity().transition(EasingTransition {
easing: easing::LINEAR,
duration: Duration::from_millis(150),
});
// Writing to opacity now triggers the animation automatically.
w
}
.into_widget()
}

State Orchestration with @AnimateMatch

Map business states (enums) to visual targets. It synchronizes multiple properties in a single optimized loop.

use ribir::prelude::*;

#[derive(Clone, Copy, PartialEq, Eq)]
enum CardStatus {
Idle,
Active,
}

fn demo() -> Widget<'static> {
fn_widget! {
let card_status = Stateful::new(CardStatus::Idle);
let mut card = @Container { size: Size::new(80., 40.) };
let opacity = card.opacity();
let transform = card.transform();

// cases! defines targets, transitions! defines routing.
let _am = @AnimateMatch {
value: card_status.clone_watcher(),
cases: cases! {
state: (opacity, transform),
CardStatus::Idle => (1.0, Transform::identity()),
CardStatus::Active => (0.7, Transform::scale(0.9, 0.9)),
},
transitions: transitions! {
(_, CardStatus::Active) => EasingTransition {
easing: easing::LINEAR,
duration: Duration::ZERO,
},
_ => EasingTransition {
easing: easing::LINEAR,
duration: Duration::from_millis(200),
},
},
};

card
}
.into_widget()
}

Lifecycle: AnimatedVisibility vs AnimatedPresence

WidgetUsagePersistence
AnimatedVisibilityToggling frequent UI (sidebars, menus)Stays mounted in the tree.
AnimatedPresenceDynamic content (lists, conditional blocks)Mounted/Unmounted structurally.

Both use the same API:

  • cases: Visual targets for true (shown) and false (hidden).
  • enter / leave: Optional transitions for each direction.

Timeline & Sequences

  • keyframes!: Script precise intermediate waypoints.
  • Stagger: Offset start times for multiple elements (e.g., cascading lists).

Position & Size Smoothing: SmoothLayout & SmoothGlobal

Ribir provides layout-integrated smoothing widgets that automatically animate geometry transitions (position and size). Unlike @Animate, which requires explicitly binding to a property, these widgets intercept the layout pipeline to interpolate from the old geometry toward the new target.

SmoothLayout

The primary widget for animating local position and size within the parent's coordinate space.

@SmoothLayout {
pos_axes: PosAxes::Pos, // Animate X and Y
size_axes: SizeAxes::Size, // Animate Width and Height
transition: EasingTransition {
easing: easing::EASE_OUT,
duration: Duration::from_millis(300),
},
// Entry animation starting values
init_x: AnchorX::left().offset(-100.),
init_size: Size::splat(0.),
@MyWidget { ... }
}
  • SizeMode: Determines if the animated size affects the layout of surrounding widgets (Layout, default) or is purely visual (Visual).
  • SizeEffect: Choose between Clip (cropping the content, default) or Scale (stretching the content) during size transitions.

SmoothGlobal

A specialized widget for global position smoothing. It tracks the widget's position in window coordinates, ensuring continuity even if the widget moves across different parent containers or subtrees.

@SmoothGlobal {
pos_axes: PosAxes::Pos,
@Child {}
}

Useful for "shared element" style transitions where a widget needs to move smoothly between two different locations in the UI tree that don't share a common coordinate system.


Summary Guide

  1. Mount/Dispose animation?AnimatedPresence (or AnimatedVisibility if static).
  2. Enum-driven visuals?@AnimateMatch.
  3. Implicit layout/geometry transitions?SmoothLayout or SmoothGlobal.
  4. Simple property glide?transition(...).
  5. Manual control?@Animate.
  6. Complex timeline?keyframes! or Stagger.