A lightweight, cross-platform animation library for Dioxus, designed to bring smooth, flexible animations to your Rust web, desktop, and mobile applications.
This repository follows Dioxus's main branch for the latest features and improvements. For production use, we recommend using the stable version from crates.io instead of directly depending on the repository.
# Recommended: Stable version from crates.io
dioxus-motion = "0.3.1"
# Development version: Follows Dioxus main branch
dioxus-motion = { git = "https://github.com/wheregmis/dioxus-motion.git", branch = "main" }Visit our Example Website to see these animations in action:
use dioxus_motion::prelude::*;
#[derive(Routable, Clone, Debug, PartialEq, MotionTransitions )]
#[rustfmt::skip]
enum Route {
#[layout(NavBar)]
#[route("/")]
#[transition(Fade)]
Home {},
#[route("/slide-left")]
#[transition(ZoomIn)]
SlideLeft {},
#[route("/slide-right")]
SlideRight {},
#[route("/slide-up")]
SlideUp {},
#[route("/slide-down")]
SlideDown {},
#[route("/fade")]
Fade {},
#[end_layout]
#[route("/:..route")]
PageNotFound { route: Vec<String> },
}And replace all your Outlet::<Route> {} with AnimatedOutlet::<Route> {} and place the layout containing OutletRouter on top with something like this
#[component]
fn NavBar() -> Element {
rsx! {
nav { id: "navbar take it",
Link { to: Route::Home {}, "Home" }
Link { to: Route::SlideLeft {}, "Blog" }
}
AnimatedOutlet::<Route> {}
}
}Each route can have its own transition effect:
Fade: Smooth opacity transitionZoomIn: Scale and fade combinationSlideLeft: Horizontal slide animation- And more!
- Also, add transitions feature to support page transitions. Example which was translated from router example of Dioxus. More detailed guide will be updated soon.
use dioxus_motion::prelude::*;
#[component]
fn PulseEffect() -> Element {
let scale = use_motion(1.0f32);
use_effect(move || {
scale.animate_to(
1.2,
AnimationConfig::new(AnimationMode::Spring(Spring {
stiffness: 100.0,
damping: 5.0,
mass: 0.5,
velocity: 1.0
}))
.with_loop(LoopMode::Infinite)
);
});
rsx! {
div {
class: "w-20 h-20 bg-blue-500 rounded-full",
style: "transform: scale({scale.get_value()})"
}
}
}Chain multiple animations together with different configurations:
let scale = use_motion(1.0f32);
// Create a bouncy sequence
let sequence = AnimationSequence::new()
.then(
1.2, // Scale up
AnimationConfig::new(AnimationMode::Spring(Spring {
stiffness: 400.0,
damping: 10.0,
mass: 1.0,
velocity: 5.0,
}))
)
.then(
0.8, // Scale down
AnimationConfig::new(AnimationMode::Spring(Spring {
stiffness: 300.0,
damping: 15.0,
mass: 1.0,
velocity: -2.0,
}))
)
.then(
1.0, // Return to original
AnimationConfig::new(AnimationMode::Spring(Spring::default()))
);
// Start the sequence
scale.animate_sequence(sequence);
// Each step in the sequence can have its own timing, easing, and spring physics configuration. Sequences can also be looped or chained with other animations.- π§ Simplified Animatable Trait: Uses standard Rust operators (
+,-,*) instead of custom methods - π Cross-Platform Support: Works on web, desktop, and mobile
- βοΈ Flexible Animation Configuration: Spring physics and tween animations
- π Custom Easing Functions: Built-in and custom easing support
- π§© Modular Feature Setup: Choose only what you need
- π‘ Simple, Intuitive API: Easy to learn and use
- π¬ Page Transitions: Smooth route transitions with the
transitionsfeature
Add to your Cargo.toml:
[dependencies]
dioxus-motion = { version = "0.3.0", optional = true, default-features = false }
[features]
default = ["web"]
web = ["dioxus/web", "dioxus-motion/web"]
desktop = ["dioxus/desktop", "dioxus-motion/desktop"]
mobile = ["dioxus/mobile", "dioxus-motion/desktop"]If you want to use page transiton dependency will look like,
[dependencies]
dioxus-motion = { version = "0.3.0", optional = true, default-features = false }
[features]
default = ["web"]
web = ["dioxus/web", "dioxus-motion/web", "dioxus-motion/transitions"]
desktop = [
"dioxus/desktop",
"dioxus-motion/desktop",
"dioxus-motion/transitions",
]
mobile = ["dioxus/mobile", "dioxus-motion/desktop", "dioxus-motion/transitions"]Choose the right feature for your platform:
web: For web applications using WASMdesktop: For desktop and mobile applicationsdefault: Web support (if no feature specified)
The simplified Animatable trait makes it easy to create custom animatable types:
use dioxus_motion::prelude::*;
#[derive(Debug, Copy, Clone, PartialEq, Default)]
struct Point3D {
x: f32,
y: f32,
z: f32,
}
// Point3D automatically implements Send + 'static since all fields are Send + 'static
// Implement standard Rust operator traits
impl std::ops::Add for Point3D {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
z: self.z + other.z,
}
}
}
impl std::ops::Sub for Point3D {
type Output = Self;
fn sub(self, other: Self) -> Self {
Self {
x: self.x - other.x,
y: self.y - other.y,
z: self.z - other.z,
}
}
}
impl std::ops::Mul<f32> for Point3D {
type Output = Self;
fn mul(self, factor: f32) -> Self {
Self {
x: self.x * factor,
y: self.y * factor,
z: self.z * factor,
}
}
}
// Implement Animatable with just two methods!
impl Animatable for Point3D {
fn interpolate(&self, target: &Self, t: f32) -> Self {
*self + (*target - *self) * t
}
fn magnitude(&self) -> f32 {
(self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
}
}
// Now you can animate 3D points!
let mut position = use_motion(Point3D::default());
position.animate_to(
Point3D { x: 10.0, y: 5.0, z: -2.0 },
AnimationConfig::new(AnimationMode::Spring(Spring::default()))
);Previous vs. New Trait Complexity:
- Before: 7 required methods (
zero,epsilon,magnitude,scale,add,sub,interpolate) - After: 2 required methods (
interpolate,magnitude) + standard Rust operators - Result: ~70% less boilerplate, more idiomatic Rust code!
use_motion<T>now requiresT: Send + 'static: Theuse_motion<T>function now requires types to implementSend + 'staticin addition toAnimatable. This enables better thread safety and resource management for animations.
- Most built-in types (
f32,Transform,Color) already satisfy these bounds - For custom types, ensure they implement
Send + 'static:- Types with non-Send fields (like
Rc<T>) will need to be refactored - Use
Arc<T>instead ofRc<T>for shared ownership in animatable types
- Types with non-Send fields (like
- Minor exports might change so just import
prelude::*if anything breaks on import
use dioxus_motion::prelude::*;
// β
This works - f32 is Send + 'static
let motion = use_motion(0.0f32);
// β
This works - custom type with Send + 'static
#[derive(Copy, Clone, Default)]
struct Point { x: f32, y: f32 } // Send + 'static automatically derived
let point_motion = use_motion(Point::default());
// β This won't compile - Rc<T> is not Send
// let bad_motion = use_motion(std::rc::Rc::new(0.0f32));
// β
Use Arc<T> instead for shared ownership
// Note: The type inside Arc must implement Animatable
#[derive(Copy, Clone, Default)]
struct SharedValue { value: f32 }
impl std::ops::Add for SharedValue {
type Output = Self;
fn add(self, other: Self) -> Self {
Self { value: self.value + other.value }
}
}
impl std::ops::Sub for SharedValue {
type Output = Self;
fn sub(self, other: Self) -> Self {
Self { value: self.value - other.value }
}
}
impl std::ops::Mul<f32> for SharedValue {
type Output = Self;
fn mul(self, factor: f32) -> Self {
Self { value: self.value * factor }
}
}
impl dioxus_motion::animations::core::Animatable for SharedValue {
fn interpolate(&self, target: &Self, t: f32) -> Self {
Self { value: self.value + (target.value - self.value) * t }
}
fn magnitude(&self) -> f32 {
self.value.abs()
}
}
let shared_motion = use_motion(SharedValue { value: 0.0 });
// β
Alternative: Use Arc to share the motion itself (not the value)
let shared_motion_handle = std::sync::Arc::new(use_motion(0.0f32));
// Now you can clone the Arc and share the motion across components
let motion_clone = shared_motion_handle.clone();- Combined
use_value_animationanduse_transform_animationintouse_motion - New animation configuration API
- Updated spring physics parameters
- Changed transform property names
use dioxus_motion::prelude::*;
// Before (v0.1.x)
let mut motion = use_value_animation(Motion::new(0.0).to(100.0));
// After (v0.2.x)
let mut value = use_motion(0.0f32);
value.animate_to(
100.0,
AnimationConfig::new(AnimationMode::Tween(Tween {
duration: Duration::from_secs(2),
easing: easer::functions::Linear::ease_in_out,
}))
);
// Before (v0.1.x)
let mut transform = use_transform_animation(Transform::default());
// After (v0.2.x)
let mut transform = use_motion(Transform::default());
transform.animate_to(
Transform::new(100.0, 0.0, 1.2, 45.0),
AnimationConfig::new(AnimationMode::Spring(Spring {
stiffness: 100.0,
damping: 10.0,
mass: 1.0,
..Default::default()
}))
);If you were using transform.get_style(), that function is removed to make the library more generic so I recommend building something like
let transform = use_motion(Transform::default());
let transform_style = use_memo(move || {
format!(
"transform: translate({}px, {}px) scale({}) rotate({}deg);",
transform.get_value().x,
transform.get_value().y,
transform.get_value().scale,
transform.get_value().rotation * 180.0 / std::f32::consts::PI
)
});
// and using the memo in the component
rsx! {
div {
class: "...",
style: "{transform_style.read()}",
// ...rest of component...
}
}.with_loop(LoopMode::Infinite)
.with_loop(LoopMode::Times(3)).with_delay(Duration::from_secs(1)).with_on_complete(|| println!("Animation complete!"))The Animatable trait allows you to animate any custom type.
Definition of Animatable Trait
pub trait Animatable:
Copy + 'static + Default +
std::ops::Add<Output = Self> +
std::ops::Sub<Output = Self> +
std::ops::Mul<f32, Output = Self>
{
fn interpolate(&self, target: &Self, t: f32) -> Self;
fn magnitude(&self) -> f32;
fn epsilon() -> f32 { 0.01 } // Default implementation
}Here's how to implement it:
#[derive(Debug, Copy, Clone)]
struct Position {
x: f32,
y: f32,
}
#[derive(Debug, Copy, Clone, PartialEq, Default)]
struct Position {
x: f32,
y: f32,
}
// Implement standard Rust operator traits
impl std::ops::Add for Position {
type Output = Self;
fn add(self, other: Self) -> Self {
Self { x: self.x + other.x, y: self.y + other.y }
}
}
impl std::ops::Sub for Position {
type Output = Self;
fn sub(self, other: Self) -> Self {
Self { x: self.x - other.x, y: self.y - other.y }
}
}
impl std::ops::Mul<f32> for Position {
type Output = Self;
fn mul(self, factor: f32) -> Self {
Self { x: self.x * factor, y: self.y * factor }
}
}
// Implement Animatable with just two methods!
impl Animatable for Position {
fn interpolate(&self, target: &Self, t: f32) -> Self {
*self + (*target - *self) * t
}
fn magnitude(&self) -> f32 {
(self.x * self.x + self.y * self.y).sqrt()
}
}- Default State: Implement
Defaulttrait for your type's neutral state - Operator Traits: Implement
Add,Sub, andMul<f32>using standard Rust operators - Magnitude: Return the square root of sum of squares for vector types
- Interpolate: Use linear interpolation for smooth transitions
- Epsilon: Uses default 0.01, or override with
with_epsilon()for custom precision
fn interpolate(&self, target: &Self, t: f32) -> Self {
let mut diff = target.angle - self.angle;
// Ensure shortest path
if diff > PI { diff -= 2.0 * PI; }
if diff < -PI { diff += 2.0 * PI; }
Self { angle: self.angle + diff * t }
}fn scale(&self, factor: f32) -> Self {
Self {
value: (self.value * factor).clamp(0.0, 1.0)
}
}Leverages the easer crate, supporting:
- Linear
- Quadratic
- Cubic
- Quartic
- And more!
- Fork the repository
- Create your feature branch
- Commit changes
- Push to the branch
- Create a Pull Request
MIT License
Please report issues on the GitHub repository with:
- Detailed description
- Minimal reproducible example
- Platform and feature configuration used
Bringing elegant, performant motion animations to Rust's web and desktop ecosystems with minimal complexity.
