Part 9 of 14 23 Jan 2026 By Raj Patil 30 min read

Part 9: Animation - Bringing Your UI to Life

Part 9 of the Freya Rust GUI series. Master the animation system with use_animation, AnimNum, AnimColor, easing functions, and create smooth, delightful interactions.

Intermediate #rust #freya #gui #animation #motion #transitions #tutorial
Building Native GUIs with Rust & Freya 9 / 14

Animation

Animations make your UI feel alive and responsive. Freya provides a built-in animation system that integrates seamlessly with its reactive system.

[!NOTE] Why Animations Matter Good animations:

  • Provide visual feedback for user actions
  • Guide attention to important elements
  • Make transitions feel smooth and natural
  • Create a polished, professional feel

use_animation Hook

The use_animation hook creates animations that integrate with Freya’s reactive system.

Basic Usage

fn animated_box() -> impl IntoElement {
    let animation = use_animation(|conf| {
        conf.on_creation(OnCreation::Run);  // Run immediately
        AnimNum::new(0.0, 200.0).time(500)  // Animate 0 to 200 in 500ms
    });

    let width = animation.get().value();

    rect()
        .width(Size::px(width))
        .height(Size::px(50.0))
        .background(Color::BLUE)
}

Animation Control

let mut animation = use_animation(|conf| {
    AnimNum::new(0.0, 100.0).time(200)
});

// Start animation (forward direction)
animation.start();

// Reverse animation
animation.reverse();

// Run in specific direction
animation.run(AnimDirection::Forward);
animation.run(AnimDirection::Reverse);

// Reset to initial state
animation.reset();

// Skip to end state
animation.finish();

// Check status
animation.is_running();
animation.has_run_yet();

Animated Value Types

AnimNum - Numeric Values

Animate any numeric property (width, height, position, opacity, scale):

AnimNum::new(0.0, 100.0)       // Animate from 0 to 100
    .time(200)                  // Duration in milliseconds
    .duration(Duration::from_secs(1))  // Or use Duration
    .ease(Ease::Out)            // Easing type
    .function(Function::Cubic)   // Easing function

AnimColor - Color Transitions

Smoothly transition between colors:

fn color_fade() -> impl IntoElement {
    let animation = use_animation(|conf| {
        conf.on_creation(OnCreation::Run);
        AnimColor::new(Color::RED, Color::BLUE).time(1000)
    });

    let color = animation.get().value();

    rect()
        .width(Size::px(100.0))
        .height(Size::px(100.0))
        .background(color)
}

AnimSequential - Chained Animations

Run animations one after another:

AnimSequential::new()
    .add(AnimNum::new(0.0, 100.0).time(200))   // First: grow to 100
    .add(AnimNum::new(100.0, 50.0).time(150))   // Then: shrink to 50
    .add(AnimNum::new(50.0, 200.0).time(300))   // Finally: grow to 200

Multiple Animations

Animate multiple values simultaneously:

fn multi_animation() -> impl IntoElement {
    let animation = use_animation(|conf| {
        conf.on_creation(OnCreation::Run);
        (
            AnimNum::new(0.0, 200.0).time(500),      // Width
            AnimNum::new(100.0, 50.0).time(500),     // Height
            AnimColor::new(Color::RED, Color::BLUE).time(500),  // Color
        )
    });

    let (width, height, color) = animation.get().value();

    rect()
        .width(Size::px(width))
        .height(Size::px(height))
        .background(color)
}

Easing

Easing makes animations feel natural. Without easing, animations move at constant speed, which feels robotic.

Ease Types

TypeDescription
Ease::InStart slow, accelerate
Ease::OutStart fast, decelerate
Ease::InOutSlow start and end, fast middle

Easing Functions

FunctionDescription
LinearConstant speed (no easing)
QuadQuadratic easing
CubicCubic easing (smooth, common)
QuartQuartic easing (more pronounced)
SineSinusoidal easing (gentle)
CircCircular easing
ExpoExponential easing (dramatic)
ElasticElastic/spring effect (bouncy)
BackOvershoots then returns
BounceBouncing effect

Visual Comparison

Linear:    ████████████████████  (boring)
EaseOut:   ████████▓▓▓▓▓░░░░░░  (natural start, smooth end)
EaseIn:    ░░░░░▓▓▓▓▓████████    (slow start, snappy end)
EaseInOut: ░░▓▓▓▓▓█████▓▓▓▓░░   (smooth both ends)

Choosing the Right Easing

// Smooth entry (elements appearing)
AnimNum::new(0.0, 100.0).ease(Ease::Out).function(Function::Cubic)

// Bouncy button (playful interactions)
AnimNum::new(1.0, 1.1).ease(Ease::Out).function(Function::Elastic)

// Smooth scroll or slide
AnimNum::new(0.0, 500.0).ease(Ease::InOut).function(Function::Quad)

// Dramatic reveal
AnimNum::new(0.0, 1.0).ease(Ease::Out).function(Function::Expo)

Animation Configuration

OnCreation - What Happens at Start

conf.on_creation(OnCreation::Nothing);   // Don't run (default)
conf.on_creation(OnCreation::Run);       // Run immediately
conf.on_creation(OnCreation::Finish);    // Skip to end state

OnFinish - What Happens at End

conf.on_finish(OnFinish::Nothing);                    // Stop (default)
conf.on_finish(OnFinish::reverse());                  // Reverse direction
conf.on_finish(OnFinish::reverse_with_delay(100));    // Reverse with delay
conf.on_finish(OnFinish::restart());                  // Loop from start
conf.on_finish(OnFinish::restart_with_delay(200));    // Loop with delay

OnChange - What Happens When Dependencies Change

conf.on_change(OnChange::Reset);    // Reset to initial (default)
conf.on_change(OnChange::Finish);   // Skip to end state
conf.on_change(OnChange::Rerun);    // Re-run animation
conf.on_change(OnChange::Nothing);  // Do nothing

Reactive Animations

Responding to State Changes

Animations automatically track state dependencies:

fn reactive_animation() -> impl IntoElement {
    let expanded = use_state(|| false);

    let animation = use_animation_with_dependencies(&expanded, |conf, is_expanded| {
        let target = if *is_expanded { 200.0 } else { 100.0 };
        AnimNum::new(100.0, target).time(300).ease(Ease::Out)
    });

    let width = animation.get().value();

    rect()
        .direction(Direction::Vertical)
        .gap(8.0)
        .child(
            Button::new()
                .on_press(move |_| expanded.toggle())
                .child("Toggle")
        )
        .child(
            rect()
                .width(Size::px(width))
                .height(Size::px(50.0))
                .background(Color::BLUE)
        )
}

Animation Examples

Fade In

fn fade_in() -> impl IntoElement {
    let animation = use_animation(|conf| {
        conf.on_creation(OnCreation::Run);
        AnimNum::new(0.0, 1.0).time(300).ease(Ease::Out)
    });

    let opacity = animation.get().value();

    rect()
        .opacity(opacity)
        .child(label().text("Fading in..."))
}

Scale on Hover

fn hover_button() -> impl IntoElement {
    let hover = use_state(|| false);

    let animation = use_animation_with_dependencies(&hover, |conf, is_hover| {
        let target = if *is_hover { 1.1 } else { 1.0 };
        AnimNum::new(1.0, target).time(150).ease(Ease::Out)
    });

    let scale = animation.get().value();

    rect()
        .scale(Scale::new(scale))
        .on_pointer_enter(move |_| hover.set(true))
        .on_pointer_leave(move |_| hover.set(false))
        .child(Button::new().child("Hover me"))
}

Slide-In Menu

fn slide_menu(is_open: State<bool>) -> impl IntoElement {
    let animation = use_animation_with_dependencies(&is_open, |conf, open| {
        let target = if *open { 0.0 } else { -300.0 };
        AnimNum::new(-300.0, target).time(250).ease(Ease::InOut)
    });

    let x_offset = animation.get().value();

    rect()
        .position(Position::Absolute)
        .left(Size::px(x_offset))
        .width(Size::px(300.0))
        .height(Size::fill())
        .background(Color::WHITE)
        .shadow(Shadow::new()
            .color(Color::BLACK.with_a(50))
            .x(4.0)
            .blur(20.0)
        )
        .padding(16.0)
        .direction(Direction::Vertical)
        .gap(8.0)
        .child(label().text("Menu Item 1"))
        .child(label().text("Menu Item 2"))
        .child(label().text("Menu Item 3"))
}

Pulsing Animation (Loop)

fn pulse() -> impl IntoElement {
    let animation = use_animation(|conf| {
        conf.on_creation(OnCreation::Run);
        conf.on_finish(OnFinish::reverse());  // Loop back and forth
        AnimNum::new(1.0, 1.2).time(500).ease(Ease::InOut)
    });

    let scale = animation.get().value();

    rect()
        .scale(Scale::new(scale))
        .background(Color::from_rgb(239, 68, 68))
        .corner_radius(50.0)
        .padding(8.0)
        .child(label().text("Live").color(Color::WHITE))
}

Animated Progress Bar

fn animated_progress(progress: f64) -> impl IntoElement {
    let animation = use_animation_with_dependencies(&progress, |conf, p| {
        AnimNum::new(0.0, *p * 100.0).time(300).ease(Ease::Out)
    });

    let width = animation.get().value();

    rect()
        .width(Size::fill())
        .height(Size::px(8.0))
        .background(Color::from_rgb(230, 230, 230))
        .corner_radius(4.0)
        .overflow(Overflow::Clip)
        .child(
            rect()
                .width(Size::px(width))
                .height(Size::fill())
                .background(Color::from_rgb(79, 70, 229))
                .corner_radius(4.0)
        )
}

Button Press Effect

fn pressable_button() -> impl IntoElement {
    let pressed = use_state(|| false);

    let animation = use_animation_with_dependencies(&pressed, |conf, is_pressed| {
        let target = if *is_pressed { 0.95 } else { 1.0 };
        AnimNum::new(1.0, target).time(50).ease(Ease::Out)
    });

    let scale = animation.get().value();

    rect()
        .scale(Scale::new(scale))
        .background(Color::from_rgb(79, 70, 229))
        .corner_radius(8.0)
        .padding_horizontal(24.0)
        .padding_vertical(12.0)
        .on_pointer_down(move |_| pressed.set(true))
        .on_pointer_up(move |_| pressed.set(false))
        .on_pointer_leave(move |_| pressed.set(false))
        .child(label().text("Press Me").color(Color::WHITE))
}

Card Flip (Rotate)

fn flip_card() -> impl IntoElement {
    let flipped = use_state(|| false);

    let animation = use_animation_with_dependencies(&flipped, |conf, is_flipped| {
        let target = if *is_flipped { 180.0 } else { 0.0 };
        AnimNum::new(0.0, target).time(400).ease(Ease::InOut)
    });

    let rotation = animation.get().value();

    rect()
        .width(Size::px(200.0))
        .height(Size::px(200.0))
        .rotate(rotation)
        .background(if flipped() {
            Color::from_rgb(79, 70, 229)
        } else {
            Color::from_rgb(234, 179, 8)
        })
        .corner_radius(12.0)
        .main_align_center()
        .cross_align_center()
        .on_pointer_down(move |_| flipped.toggle())
        .child(
            label()
                .text(if flipped() { "Back" } else { "Front" })
                .font_size(24.0)
                .color(Color::WHITE)
        )
}

Animation Best Practices

1. Keep Animations Fast

Most UI animations should be 150-300ms. Longer animations feel sluggish.

// Good: Quick and responsive
AnimNum::new(0.0, 1.0).time(200)

// Bad: Too slow for UI
AnimNum::new(0.0, 1.0).time(1000)

2. Use Appropriate Easing

// Elements appearing: ease out
.ease(Ease::Out)

// Elements disappearing: ease in
.ease(Ease::In)

// Continuous motion: ease in-out
.ease(Ease::InOut)

3. Don’t Overdo It

4. Respect User Preferences

Some users prefer reduced motion. Consider checking for this preference (platform-dependent).

5. Test Performance

Complex animations can impact performance. Test on lower-end devices.


Summary

In this tutorial, you learned:


Previous: Part 8: Built-in Components ←

Next: Part 10: Theming →

In the next tutorial, we’ll explore theming - how to create consistent, beautiful color schemes and support light/dark modes.