dioxus-animate

Dioxus crates.io Downloads crates.io Size License crates.io

✨ Time-based CSS class animations for Dioxus — think CSS keyframes, but driven by your app's logic.

dioxus-animate gives you one ergonomic macro to sequence CSS class additions and removals on a timeline. You say "at 300ms add opacity-100, at 500ms remove opacity-0", it runs the sequence asynchronously against a real DOM element. No animation runtime, no state machine to wire up — you already have the CSS, this just toggles the classes for you at the right moments.

Plays nicely with utility-class frameworks like Tailwind, where the transitions live in the classes and all you need is something to flip them on cue.

🖤 Features

  • use_animate! — one declarative macro, your whole sequence reads top-to-bottom like keyframes

  • add / remove — the only two verbs you need; classes go on, classes come off

  • Grouped ops — fire several class changes at the exact same tick with (...)

  • Async under the hood — sequences run on Dioxus' task runtime, nothing blocks

  • Two ways to target — by mounted element reference, or by plain element id


Table of Contents

📦 Installation

Add it to your Cargo.toml:

TOML
[dependencies]
dioxus-animate = "0.3"

Or let cargo do the editing:

BASH
cargo add dioxus-animate

The latest version targets Dioxus 0.7 and the web (WASM) renderer — see the compatibility table for the version mapping. Built on the Rust 2024 edition, so you'll want a recent stable toolchain.

🧪 Usage

Basic animation sequence

Reach for the use_animate! macro to lay out timed CSS class operations, then call start on a mounted element:

RUST
use dioxus::prelude::*;
use dioxus_animate::prelude::*;

#[component]
fn App() -> Element {
    let mut element_ref = use_signal(|| None);

    let animation = use_animate!(
        300 => add("opacity-100"),
        500 => remove("opacity-0"),
        1000 => add("scale-110"),
    );

    let start_animation = move |_| {
        animation.start(element_ref.into());
    };

    rsx! {
        div {
            class: "opacity-0 transition-all duration-300",
            onmounted: move |event| element_ref.set(Some(event.data())),
            onclick: start_animation,
            "Click me to animate!"
        }
    }
}

Grouped operations

Wrap operations in parentheses (separated by ;) to fire them on the same tick:

RUST
let animation = use_animate!(
    0 => add("animate-pulse"),
    500 => (
        add("bg-blue-500");
        remove("bg-gray-200")
    ),
    1000 => remove("animate-pulse"),
);

Complex sequences

Chain as many steps as you like — single ops and groups mix freely:

RUST
let animation = use_animate!(
    0 => add("opacity-100"),
    200 => remove("opacity-0"),
    400 => add("scale-105"),
    600 => (
        add("rotate-3");
        add("shadow-lg")
    ),
    1000 => remove("scale-105 rotate-3"),
    1200 => add("scale-100"),
);

One thing to keep in mind: timestamps are cumulative from the start and must climb in ascending order (same as you'd write CSS keyframes). The runtime sleeps for the gap between each step, so a step that goes backwards in time isn't a thing.

Trigger via element reference

Capture the element on onmounted, then hand its reference to start:

RUST
// grab it when the node mounts
onmounted: move |event| element_ref.set(Some(event.data())),

// fire it from any handler
animation.start(element_ref.into());

Trigger via element id

Don't want to juggle references? Target by id with start_for_id instead — handy when the element lives somewhere awkward to thread a signal to:

RUST
let animation = use_animate!(
    300 => add("opacity-100"),
    500 => remove("opacity-0"),
);

let trigger_animation = move |_| {
    animation.start_for_id("target");
};

rsx! {
    div {
        id: "target",
        class: "opacity-0 transition-all duration-300",
        onclick: trigger_animation,
        "Click me to animate!"
    }
}

Heads up: start_for_id expects the element to exist in the DOM at call time — it looks the node up by id and will panic if there's nothing there, so trigger it after the element has mounted.

🧠 How It Works

  1. Defineuse_animate! parses your time => operation lines into an ordered list of (ms, Operation) pairs

  2. Mount — capture the element reference via onmounted (or skip it and target by id)

  3. Triggerstart(...) / start_for_id(...) spawns an async task on Dioxus' runtime

  4. Execute — the task sleeps to each timestamp in turn and toggles the classes on the live DOM element

Time values are in milliseconds, cumulative from the start of the sequence.

📐 API Reference

use_animate!

Builds an animation sequence:

RUST
use_animate!(
    time_ms => operation,
    time_ms => operation,
    // ...
);

Operations:

  • add("class-names") — adds CSS classes (space-separated string, multiple classes welcome)

  • remove("class-names") — removes CSS classes (same deal)

  • (op1; op2; ...) — groups operations to run on the same tick

Time values:

  • expressed in milliseconds

  • cumulative from animation start

  • must be in ascending order (think CSS keyframes)

UseAnimate::start

RUST
animation.start(element_ref.into());

Runs the sequence against a mounted element. Takes a ReadSignal<Option<Rc<MountedData>>> — in practice the Signal you filled on onmounted, with .into(). If the signal is still None, the call is a no-op (it just won't animate).

UseAnimate::start_for_id

RUST
animation.start_for_id("my-element");

Runs the sequence against the element with the given id. Convenient when you'd rather not hold a reference — just make sure the element is in the DOM when you call it (it panics if the id isn't found).

🛠️ Compatibility

Dioxus versiondioxus-animate version
0.70.3
0.60.2

A couple of things worth knowing:

  • Web / WASM only — it reaches for web-sys, gloo and Dioxus' web event APIs, so it runs in the browser renderer (it isn't wired up for desktop/mobile).

  • Rust 2024 edition — you'll want a recent stable toolchain.

📁 Repo & Contributions

🛠️ Repo: https://github.com/dsplce-co/dioxus-animate
📦 Crate: https://crates.io/crates/dioxus-animate

Contributions, issues, ideas? Hit us up 🖤

📄 License

MIT or Apache-2.0, at your option.