Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Motion Containers

The motion() element provides declarative enter/exit animations for content. It’s ideal for animated lists, page transitions, and conditional rendering.

Basic Usage

#![allow(unused)]
fn main() {
use blinc_layout::motion::motion;

motion()
    .fade_in(300)     // Duration in milliseconds
    .child(my_content())
}

Animation Presets

Fade

#![allow(unused)]
fn main() {
motion()
    .fade_in(300)
    .fade_out(200)
    .child(content)
}

Scale

#![allow(unused)]
fn main() {
motion()
    .scale_in(300)    // Scale from 0 to 1
    .scale_out(200)   // Scale from 1 to 0
    .child(content)
}

Slide

#![allow(unused)]
fn main() {
use blinc_layout::motion::SlideDirection;

motion()
    .slide_in(SlideDirection::Left, 300)
    .slide_out(SlideDirection::Right, 200)
    .child(content)

// Available directions:
// SlideDirection::Top
// SlideDirection::Bottom
// SlideDirection::Left
// SlideDirection::Right
}

Bounce

#![allow(unused)]
fn main() {
motion()
    .bounce_in(400)   // Bouncy entrance
    .bounce_out(200)
    .child(content)
}

Pop

#![allow(unused)]
fn main() {
motion()
    .pop_in(300)      // Scale with overshoot
    .pop_out(200)
    .child(content)
}

Combining Animations

Apply multiple effects:

#![allow(unused)]
fn main() {
motion()
    .fade_in(300)
    .scale_in(300)
    .child(content)
}

Staggered Lists

Animate list items with delays between each:

#![allow(unused)]
fn main() {
use blinc_layout::motion::{motion, StaggerConfig, AnimationPreset};

let items = vec!["Item 1", "Item 2", "Item 3", "Item 4"];

motion()
    .stagger(
        StaggerConfig::new(100, AnimationPreset::fade_in(300))
    )
    .children(
        items.iter().map(|item| {
            div()
                .p(12.0)
                .bg(Color::rgba(0.2, 0.2, 0.25, 1.0))
                .child(text(*item).color(Color::WHITE))
        })
    )
}

Stagger Configuration

#![allow(unused)]
fn main() {
StaggerConfig::new(delay_ms, preset)
    .reverse()          // Animate last to first
    .from_center()      // Animate from center outward
    .limit(10)          // Only stagger first N items
}

Stagger Directions

#![allow(unused)]
fn main() {
// Forward (default): 0, 1, 2, 3, 4...
StaggerConfig::new(100, preset)

// Reverse: 4, 3, 2, 1, 0...
StaggerConfig::new(100, preset).reverse()

// From center: 2, 1/3, 0/4 (for 5 items)
StaggerConfig::new(100, preset).from_center()
}

Binding to AnimatedValue

For continuous animation control, bind motion transforms to AnimatedValue:

#![allow(unused)]
fn main() {
fn pull_to_refresh(ctx: &WindowedContext) -> impl ElementBuilder {
    let offset_y = ctx.use_animated_value(0.0, SpringConfig::wobbly());
    let icon_scale = ctx.use_animated_value(0.5, SpringConfig::snappy());
    let icon_opacity = ctx.use_animated_value(0.0, SpringConfig::snappy());

    stack()
        // Refresh icon (behind content)
        .child(
            motion()
                .scale(icon_scale.clone())
                .opacity(icon_opacity.clone())
                .child(refresh_icon())
        )
        // Content (translates down to reveal icon)
        .child(
            motion()
                .translate_y(offset_y.clone())
                .child(content_list())
        )
}
}

Example: Animated Card List

Use a stateful element with .deps() to react to visibility state changes:

#![allow(unused)]
fn main() {
fn animated_card_list(ctx: &WindowedContext) -> impl ElementBuilder {
    let show_cards = ctx.use_state_keyed("show_cards", || true);
    let button_handle = ctx.use_state(ButtonState::Idle);

    stateful(button_handle)
        .flex_col()
        .gap(16.0)
        .deps(&[show_cards.signal_id()])
        .on_state(move |state, container| {
            let visible = show_cards.get();
            let label = if visible { "Hide Cards" } else { "Show Cards" };

            let bg = match state {
                ButtonState::Idle => Color::rgba(0.3, 0.5, 0.9, 1.0),
                ButtonState::Hovered => Color::rgba(0.4, 0.6, 1.0, 1.0),
                _ => Color::rgba(0.3, 0.5, 0.9, 1.0),
            };

            // Build content based on visibility
            let mut content = div()
                .bg(bg)
                .px(16.0)
                .py(8.0)
                .rounded(8.0)
                .child(text(label).color(Color::WHITE));

            container.merge(content);
        })
        .on_click(move |_| {
            show_cards.update(|v| !v);
        })
        .child(card_list(ctx))
}

fn card_list(ctx: &WindowedContext) -> impl ElementBuilder {
    // Cards with staggered animation
    motion()
        .stagger(StaggerConfig::new(80, AnimationPreset::fade_in(300)))
        .children(
            (0..5).map(|i| {
                div()
                    .w(300.0)
                    .p(16.0)
                    .rounded(12.0)
                    .bg(Color::rgba(0.15, 0.15, 0.2, 1.0))
                    .child(text(&format!("Card {}", i + 1)).color(Color::WHITE))
            })
        )
}
}

Example: Page Transition

Use a custom state type for page navigation:

#![allow(unused)]
fn main() {
use blinc_layout::stateful::{stateful, StateTransitions};

#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum Page {
    Home,
    Settings,
    Profile,
}

// Pages don't auto-transition - we change them programmatically
impl StateTransitions for Page {
    fn on_event(&self, _event: u32) -> Option<Self> {
        None  // No automatic transitions
    }
}

fn page_transition(ctx: &WindowedContext) -> impl ElementBuilder {
    let page_handle = ctx.use_state(Page::Home);

    stateful(page_handle.clone())
        .w_full()
        .h_full()
        .on_state(move |page, container| {
            // Render different content based on current page
            let content = match page {
                Page::Home => div().child(text("Home Page").color(Color::WHITE)),
                Page::Settings => div().child(text("Settings Page").color(Color::WHITE)),
                Page::Profile => div().child(text("Profile Page").color(Color::WHITE)),
            };

            container.merge(
                div()
                    .child(
                        motion()
                            .fade_in(200)
                            .slide_in(SlideDirection::Right, 200)
                            .child(content)
                    )
            );
        })
}

// Navigate programmatically
fn nav_button(ctx: &WindowedContext, target: Page, label: &str) -> impl ElementBuilder {
    let page_handle = ctx.use_state(Page::Home);  // Same handle
    let handle = ctx.use_state_for(label, ButtonState::Idle);

    stateful(handle)
        .px(16.0)
        .py(8.0)
        .rounded(8.0)
        .on_state(|state, div| {
            let bg = match state {
                ButtonState::Idle => Color::rgba(0.3, 0.5, 0.9, 1.0),
                ButtonState::Hovered => Color::rgba(0.4, 0.6, 1.0, 1.0),
                _ => Color::rgba(0.3, 0.5, 0.9, 1.0),
            };
            div.set_bg(bg);
        })
        .on_click(move |_| {
            page_handle.set(target);
        })
        .child(text(label).color(Color::WHITE))
}
}

Motion vs Manual Animation

FeatureMotionAnimatedValue
SetupDeclarativeImperative
ControlPreset-basedFull control
Enter/ExitBuilt-inManual
ListsStagger supportManual delays
Use caseTransitionsInteractive

Use motion for:

  • List item animations
  • Page transitions
  • Conditional content
  • Staggered reveals

Use AnimatedValue for:

  • Drag interactions
  • Hover effects
  • Custom physics
  • Continuous binding