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
| Type | Description |
|---|---|
Ease::In | Start slow, accelerate |
Ease::Out | Start fast, decelerate |
Ease::InOut | Slow start and end, fast middle |
Easing Functions
| Function | Description |
|---|---|
Linear | Constant speed (no easing) |
Quad | Quadratic easing |
Cubic | Cubic easing (smooth, common) |
Quart | Quartic easing (more pronounced) |
Sine | Sinusoidal easing (gentle) |
Circ | Circular easing |
Expo | Exponential easing (dramatic) |
Elastic | Elastic/spring effect (bouncy) |
Back | Overshoots then returns |
Bounce | Bouncing 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
- Avoid animating everything
- Use animations purposefully (feedback, transitions)
- Keep subtle animations subtle
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:
- use_animation - The main animation hook
- AnimNum - Animating numeric values
- AnimColor - Animating color transitions
- AnimSequential - Chaining animations
- Easing - Making animations feel natural
- Configuration - OnCreation, OnFinish, OnChange
- Reactive animations - Responding to state changes
- Practical examples - Fade, scale, slide, pulse, progress
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.