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(show_cards: State<bool>) -> impl ElementBuilder {
    stateful::<ButtonState>()
        .flex_col()
        .gap(16.0)
        .deps([show_cards.signal_id()])
        .on_state(move |ctx| {
            let visible = show_cards.get();
            let label = if visible { "Hide Cards" } else { "Show Cards" };

            let bg = match ctx.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().bg(bg).px(16.0).py(8.0).rounded(8.0)
                .child(text(label).color(Color::WHITE))
        })
        .on_click(move |_| {
            show_cards.update(|v| !v);
        })
        .child(card_list())
}

fn card_list() -> 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, use_shared_state};

#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
enum Page {
    #[default]
    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(current_page: State<u8>) -> impl ElementBuilder {
    stateful::<NoState>()
        .w_full()
        .h_full()
        .deps([current_page.signal_id()])
        .on_state(move |_ctx| {
            // Render different content based on current page signal
            let content = match current_page.get() {
                0 => div().child(text("Home Page").color(Color::WHITE)),
                1 => div().child(text("Settings Page").color(Color::WHITE)),
                _ => div().child(text("Profile Page").color(Color::WHITE)),
            };

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

// Navigate programmatically using a shared signal
fn nav_button(current_page: State<u8>, target: u8, label: &str) -> impl ElementBuilder {
    stateful::<ButtonState>()
        .px(16.0)
        .py(8.0)
        .rounded(8.0)
        .on_state(|ctx| {
            let bg = match ctx.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().bg(bg)
        })
        .on_click(move |_| {
            current_page.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