Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Augani/kael/llms.txt

Use this file to discover all available pages before exploring further.

Kael offers two complementary animation systems: implicit transitions that activate automatically whenever a style property changes, and explicit animations you compose with the Animation struct and attach to any element. Both systems run on the GPU render thread, so they stay smooth even while your application logic is busy. For self-playing vector animations, Kael also integrates Lottie through the lottie() element.

Implicit style transitions

You do not need to write any animation code to get smooth transitions between states. Whenever a div() or other styled element changes one of the following properties between renders, Kael interpolates the value over 150 ms with an ease-out curve automatically:
  • opacity
  • rotate
  • scale / scale_xy
  • transform_origin
  • background / bg
  • border_color
// These properties animate to their new values automatically
div()
    .opacity(if hovered { 1.0 } else { 0.6 })
    .scale(if pressed { 0.97 } else { 1.0 })
    .bg(if active { theme.colors.primary } else { theme.colors.surface })
Implicit transitions only fire when the element retains the same element identity across renders. Use a stable .id() on elements that conditionally appear to ensure continuity.

The Animation struct

For explicit, timed animations you use Animation::new() and attach it to an element with the AnimationExt trait, which is automatically implemented for every IntoElement.
pub struct Animation { /* … */ }

impl Animation {
    pub fn new(duration: Duration) -> Self
    pub fn easing(self, easing: Easing) -> Self
    pub fn with_easing(self, easing: impl Fn(f32) -> f32 + 'static) -> Self
    pub fn delay(self, delay: Duration) -> Self
    pub fn repeat(self, repeat: Repeat) -> Self
    pub fn repeat_forever(self) -> Self
}

Easing functions

The Easing enum covers the most common curves as well as advanced options:
pub enum Easing {
    Linear,
    EaseIn,           // quadratic ease-in
    EaseOut,          // quadratic ease-out
    EaseInOut,        // quadratic ease-in-out
    CubicBezier(f32, f32, f32, f32),  // CSS-style control points
    Spring {
        stiffness: f32,
        damping: f32,
        mass: f32,
    },
    Custom(Rc<dyn Fn(f32) -> f32>),
}
Use the .easing() builder for named curves, or .with_easing() for an inline closure:
use std::time::Duration;

// Named easing
let anim = Animation::new(Duration::from_millis(300))
    .easing(Easing::EaseOut);

// CSS cubic-bezier equivalent of "ease"
let anim = Animation::new(Duration::from_millis(400))
    .easing(Easing::CubicBezier(0.25, 0.1, 0.25, 1.0));

// Custom closure
let anim = Animation::new(Duration::from_millis(500))
    .with_easing(|t| t * t * (3.0 - 2.0 * t)); // smoothstep

Spring animations

Kael implements a damped spring curve natively. Tune stiffness, damping, and mass to match the feel of your interface:
let bouncy = Animation::new(Duration::from_millis(600))
    .easing(Easing::Spring {
        stiffness: 300.0,
        damping: 20.0,
        mass: 1.0,
    });

Repeat behavior

pub enum Repeat {
    Once,          // play once and stop
    Count(u32),    // play N times and stop
    Forever,       // loop indefinitely
}
// Play three times
Animation::new(Duration::from_millis(200))
    .repeat(Repeat::Count(3))

// Loop forever
Animation::new(Duration::from_secs(1))
    .repeat_forever()

Attaching animations to elements

The AnimationExt trait adds .with_animation(), .with_animations(), and .with_keyframes() to every element.

.with_animation()

The animator closure receives a f32 delta in the range 0.0..=1.0 and returns the modified element:
div()
    .w_32()
    .h_32()
    .bg(theme.colors.primary)
    .with_animation(
        "fade-in",
        Animation::new(Duration::from_millis(300)).easing(Easing::EaseOut),
        |el, delta| el.opacity(delta),
    )

.with_animations() — multiple sequential animations

When you have a list of Animation values, the animator also receives the zero-based index of the currently active animation:
div()
    .with_animations(
        "entrance",
        vec![
            Animation::new(Duration::from_millis(200)).easing(Easing::EaseOut),
            Animation::new(Duration::from_millis(200))
                .easing(Easing::EaseIn)
                .delay(Duration::from_millis(200)),
        ],
        |el, index, delta| match index {
            0 => el.opacity(delta),
            1 => el.scale(0.9 + 0.1 * delta),
            _ => el,
        },
    )

Cancellable animations

Use .with_cancellable_animation() when you need to stop the animation early and snap it to its final state:
let (animated, handle) = div()
    .with_cancellable_animation(
        "spinner",
        Animation::new(Duration::from_secs(1)).repeat_forever(),
        |el, delta| el.rotate(delta * 360.0),
    );

// Later, when loading is complete:
handle.cancel();

Keyframe animations

keyframes() lets you declaratively describe style targets at normalized time offsets (0.0 = start, 1.0 = end). Keyframes support opacity, scale, scale_xy, and rotate.
pub fn keyframes() -> Keyframes

impl Keyframes {
    pub fn at(self, progress: f32, build: impl FnOnce(StyledKeyframe) -> StyledKeyframe) -> Self
}

pub struct StyledKeyframe { /* … */ }

impl StyledKeyframe {
    pub fn opacity(self, opacity: f32) -> Self
    pub fn scale(self, factor: f32) -> Self
    pub fn scale_xy(self, x: f32, y: f32) -> Self
    pub fn rotate(self, degrees: f32) -> Self
}
Use .with_keyframes() or the shorthand .animation() to bind keyframes to an Animation:
div()
    .w_16()
    .h_16()
    .bg(theme.colors.primary)
    .rounded_full()
    .with_keyframes(
        "pulse",
        keyframes()
            .at(0.0, |k| k.scale(1.0).opacity(1.0))
            .at(0.5, |k| k.scale(1.15).opacity(0.7))
            .at(1.0, |k| k.scale(1.0).opacity(1.0)),
        Animation::new(Duration::from_secs(2)).repeat_forever(),
    )

Animation sequences

AnimationSequence chains multiple Animation values end-to-end, with optional overlap:
pub struct AnimationSequence { /* … */ }

impl AnimationSequence {
    pub fn new() -> Self
    pub fn then(self, animation: Animation) -> Self
    pub fn then_for(self, duration: Duration) -> Self
    pub fn with_overlap(self, overlap: Duration) -> Self
    pub fn into_animations(self) -> Vec<Animation>
    pub fn animations(&self) -> &[Animation]
}
let sequence = AnimationSequence::new()
    .then(Animation::new(Duration::from_millis(200)).easing(Easing::EaseOut))
    .then(Animation::new(Duration::from_millis(150)).easing(Easing::EaseIn))
    .with_overlap(Duration::from_millis(50));

div()
    .with_animation_sequence(
        "slide-bounce",
        sequence,
        |el, index, delta| match index {
            0 => el.opacity(delta),
            1 => el.scale(1.0 - 0.05 * (1.0 - delta)),
            _ => el,
        },
    )

Lottie animations

For pre-authored vector animations, use the lottie() element. It loads .lottie or Bodymovin JSON files off-thread and renders them through Kael’s GPU atlas.
lottie("assets/success.lottie")
    .autoplay()
    .loop_mode(LoopMode::Once)
    .w_48()
    .h_48()

// Ping-pong loop
lottie("assets/idle.lottie")
    .autoplay()
    .ping_pong()
    .w_32()
    .h_32()
See the Elements guide for the full lottie() API including loading and fallback callbacks.

Build docs developers (and LLMs) love