# The Wings Framework book

By DoubleG Arts · 12 October 2026 · Wings 0.1.0 / Bevy 0.20

```toml
[dependencies]
bevy = "0.20"
bevy_wings = "0.1"
```

## 1. Your first Wings application

Wings Framework builds application interfaces with Bevy’s entity component system. A button is an entity with components for appearance, input and semantics. A screen is a hierarchy of those entities. The framework adds reusable scene components and systems, while leaving your application state visible to ordinary Bevy queries. This book targets the repository’s 0.1.0 APIs, Bevy 0.20 and Rust 1.97.1 or newer.

Start with the repository examples rather than assembling every feature at once. Clone https://github.com/doublegarts/bevy_wings, enter the checkout and run cargo run --example widgets. For the complete application, run cargo run -p wings-gallery. The UI examples need a display and working graphics driver. Use -- --headless for a fixed-frame terminal smoke run, while remembering that it cannot validate the rendered appearance.

For a new application, use Bevy 0.20 and bevy_wings 0.1 from crates.io. All 13 library packages are available at version 0.1.0. Register DefaultPlugins before WingsUiPlugins and spawn Camera2d before your first UI scene. Dependency injection and navigation have application-specific configuration and are registered separately.

The code below is the working widgets example from the repository. Its support module configures the camera, rendering features and headless mode. When studying this book, run these examples in that checkout; copying a fragment into an otherwise empty project does not also copy the support module. The linked source includes the tests omitted from these excerpts.

```rust
//! Interactive controls, text input, a progress indicator and snackbars.
mod support;
use bevy::prelude::*;
use bevy_wings::prelude::*;

fn main() {
    support::run("Wings: widgets", configure);
}
fn configure(app: &mut App) {
    app.add_systems(Startup, setup);
}
fn setup(mut commands: Commands) {
    commands.spawn_scene(bsn! {
        @WingsScaffold
        Children [
            @WingsScrollColumn { @padding: 24.0, @gap: 16.0 }
            Children [
                @WingsText { @text: "Widget playground", @role: TypeRole::HeadlineMedium }
                --
                @WingsCheckbox { @label: "Checkbox" }
                --
                @WingsSwitch { @label: "Switch" }
                --
                @WingsRadioGroup { @gap: 8.0 }
                Children [
                    @WingsRadioButton { @label: "Option A", @checked: true }
                    --
                    @WingsRadioButton { @label: "Option B" }
                ]
                --
                @WingsSlider { @value: 0.5 }
                on(|change: On<ValueChange<f32>>| println!("Slider: {:.2}", change.value))
                --
                @WingsTextField { @label: "Type here", @value: "Hello" }
                on(|change: On<WingsTextChanged>| println!("Text: {}", change.value))
                --
                @WingsLinearProgress { @progress: 0.65 }
                --
                @WingsButton { @text: "Show snackbar" }
                on(|_: On<Activate>, mut messages: MessageWriter<WingsShowSnackbar>| {
                    messages.write(WingsShowSnackbar::new("Button activated").with_action("OK"));
                })
            ]
        ]
    });
}
```

Source: https://github.com/doublegarts/bevy_wings/blob/main/examples/widgets.rs

**Try it:** Run the widgets example, activate the snackbar button, drag a slider and use Tab to move keyboard focus. Repeat with -- --headless.

**Check:** A graphical run responds to input; a headless run completes without opening a window. These checks exercise different parts of the application.

## 2. Compose screens from entities

The bsn! macro describes a scene. A declaration such as @WingsText supplies a scene component’s initial properties; Children defines the hierarchy. The -- separator starts a sibling. That distinction matters: a misplaced separator can attach a control to the preceding widget instead of placing it next to it.

Use WingsColumn and WingsRow for common layout, WingsCard for a grouped surface, and WingsScaffold for the outer application structure. Choose properties for the widget’s initial behavior, then add marker components when you need to find a particular entity later. A marker is more reliable than searching for a visible label that might be translated or edited.

A modifier chain is a value containing layout and style choices. Bind it to a variable and place that value alongside the scene component that it should affect. fill_max_width fills available width; padding adds inner space; background and shape select semantic theme roles. Chaining hover_scale adds an interaction animation without changing your business logic.

At runtime, query and change the owning component rather than rebuilding the entire screen each frame. Wings folds a changed modifier into the underlying Bevy UI components. If the modifier does not change, its system can skip that entity. This is a useful reason to keep static decoration separate from frequently changing model data.

```rust
//! Compose layout, theme paint, shape, elevation and hover scale on a node.
mod support;
use bevy::prelude::*;
use bevy_wings::prelude::*;

#[derive(Component, Default, Clone)]
struct ModifiedCard;
fn main() {
    support::run("Wings: modifiers", configure);
}
fn configure(app: &mut App) {
    app.add_systems(Startup, setup);
}
fn setup(mut commands: Commands) {
    let style = modifier()
        .width(360.0)
        .padding(32.0)
        .background(ColorRole::PrimaryContainer)
        .shape(ShapeRole::Large)
        .elevation(3)
        .hover_scale(1.08);
    commands.spawn_scene(bsn! {
        @WingsScaffold
        Children [
            @WingsColumn { @padding: 48.0, @gap: 24.0 }
            Children [
                @WingsText { @text: "Hover the styled card", @role: TypeRole::HeadlineMedium }
                --
                Node style ModifiedCard bevy::picking::hover::Hovered
                Children [ @WingsText { @text: "A modifier chain becomes ordinary ECS components." } ]
            ]
        ]
    });
}
```

Source: https://github.com/doublegarts/bevy_wings/blob/main/examples/modifiers.rs

**Try it:** Add a second card with a different padding value. Give it a marker component, then change its modifier from a Bevy system.

**Check:** Only the marked card changes. The other card remains a separate entity and retains its own modifier chain.

## 3. Theme with roles instead of colors

A theme resource connects semantic choices to a concrete light or dark palette. A button expresses Primary and OnPrimary rather than storing an application-wide hard-coded color pair. The painter resolves those roles and writes the appropriate Bevy colors. Changing the theme resource repaints existing entities; you do not need to respawn them.

WingsTheme::from_seed builds a palette from a seed color. Supply a configured WingsThemePlugin through the WingsUiPlugins group, and use theme.toggle_dark() when a user asks to switch modes. Color roles cover surfaces, text, outlines and emphasis. Typography, corner shapes, spacing, elevation and motion have their own token sets.

For handwritten Bevy nodes, add WingsBackground, WingsForeground or WingsBorderPaint to opt into painting. Use WingsContentColor and inheritance when several descendants should share a content color. A widget supplies the common paint components for you, so start with the widget before adding custom paint behavior.

A fixed color can be appropriate for a photograph, a brand illustration or an exact status indicator. Do not use it as an accidental substitute for a semantic role. Check both schemes and keyboard focus when customizing a palette; a visually pleasant seed is not a complete accessibility test.

```rust
//! Toggle light/dark mode and choose a seeded color scheme.
mod support;
use bevy::prelude::*;
use bevy_wings::prelude::*;

fn main() {
    support::run("Wings: theme", configure);
}
fn configure(app: &mut App) {
    app.add_systems(Startup, setup);
}
fn setup(mut commands: Commands) {
    commands.spawn_scene(bsn! {
        @WingsScaffold
        Children [
            @WingsColumn { @padding: 32.0, @gap: 16.0 }
            Children [
                @WingsText { @text: "Theme roles update together", @role: TypeRole::HeadlineMedium }
                --
                @WingsButton { @text: "Toggle light / dark" }
                on(|_: On<Activate>, mut theme: ResMut<WingsTheme>| theme.toggle_dark())
                --
                @WingsButton { @text: "Use a green seed", @variant: WingsButtonVariant::Outlined }
                on(|_: On<Activate>, mut theme: ResMut<WingsTheme>| {
                    let mode = theme.mode;
                    *theme = WingsTheme::from_seed(Color::srgb(0.15, 0.65, 0.35));
                    theme.mode = mode;
                })
                --
                @WingsCard Children [ @WingsText { @text: "Surface, content, border and typography come from roles." } ]
            ]
        ]
    });
}
```

Source: https://github.com/doublegarts/bevy_wings/blob/main/examples/theme.rs

**Try it:** Switch between light and dark in the theme example, then change the seed color. Inspect backgrounds, outlines and text.

**Check:** Existing widgets repaint in place. Text remains readable and focus remains visible in both schemes.

## 4. Adapt layout to the available window

Responsiveness starts with available logical width rather than device names. The viewport class changes when the window crosses the configured breakpoints. A desktop window can be narrow; a tablet can be wide. Designing around those classes keeps layout decisions tied to actual space.

WingsResponsiveNode applies layout patches for width classes, WingsResponsiveVisible changes visibility, and responsive grid columns change the number of items in a row. Use those tools for presentation decisions. Keep the underlying records and domain rules unchanged when a window changes size.

The focused example changes its grid around 600, 840 and 1200 logical pixels. Resize slowly across those boundaries and watch how the same content reflows. A navigation bar can become a rail on a wide display. Adaptive list/detail navigation can show two panes without inventing a different detail screen.

A responsive page still needs minimum usable sizes, sensible wrapping and scroll behavior. Long translated labels are a useful stress test. Prefer a scroll container for long content rather than shrinking every control until the page fits. For custom breakpoints, document the intended classes and validate both sides of each boundary.

```rust
//! Resize the window: the grid grows from one column to four.
mod support;
use bevy::prelude::*;
use bevy_wings::prelude::*;

#[derive(Component, Default, Clone)]
struct ResponsiveGrid;
fn main() {
    support::run("Wings: responsive layout", configure);
}
fn configure(app: &mut App) {
    app.add_systems(Startup, setup);
}
fn setup(mut commands: Commands) {
    let columns = WingsResponsiveColumns(WingsResponsive::new(1).medium(2).expanded(3).large(4));
    commands.spawn_scene(bsn! {
        @WingsScaffold
        Children [
            @WingsColumn { @padding: 24.0, @gap: 16.0 }
            Children [
                @WingsText { @text: "Resize me: <600 / 600 / 840 / 1200 px", @role: TypeRole::TitleLarge }
                --
                @WingsGrid { @gap: 16.0 } columns ResponsiveGrid
                Children [
                    @WingsCard Children [ @WingsText { @text: "One" } ]
                    --
                    @WingsCard Children [ @WingsText { @text: "Two" } ]
                    --
                    @WingsCard Children [ @WingsText { @text: "Three" } ]
                    --
                    @WingsCard Children [ @WingsText { @text: "Four" } ]
                ]
            ]
        ]
    });
}
```

Source: https://github.com/doublegarts/bevy_wings/blob/main/examples/responsive.rs

**Try it:** Run the responsive example and resize just below and above each breakpoint. Add a long label to one item.

**Check:** Column counts change at the intended boundaries and the long label wraps without covering another control.

## 5. State, bindings and user intent

A Wings view model is a component on the screen root. It contains the presentation state for that screen. Implement WingsViewModel and register its type. Child bindings find the closest view model of the requested type by walking the entity hierarchy, so two screens can use the same model type without sharing an instance.

In the counter example, bind_text derives a label from Counter.count. on_activate converts an Activate event into a mutation of Counter. The mutation participates in Bevy change detection; the binding recalculates and writes text only when necessary. This is the complete reactive path, with no parallel observer graph outside the ECS.

Use bind_enabled for actions that depend on state, bind_visible for optional content and bind_checked for selections. For another component type, use a generic binding and register it. Derived display strings belong near the binding; validation and business operations belong in the domain layer.

Keep transient notifications separate from durable state. WingsEffects queues one-shot events that can be drained into Bevy messages. A count belongs in the model; a snackbar indicating that a reset occurred is an effect. A navigation entry keeps its model alive while that entry remains on the back stack, then clears it when despawned.

```rust
//! A component view model, change-driven text bindings and button intents.
mod support;
use bevy::prelude::*;
use bevy_wings::prelude::*;

#[derive(Component, Default, Clone)]
struct Counter {
    count: u32,
}
impl WingsViewModel for Counter {}
#[derive(Component, Default, Clone)]
struct IncrementButton;
fn main() {
    support::run("Wings: MVVM", configure);
}
fn configure(app: &mut App) {
    app.register_view_model::<Counter>()
        .add_systems(Startup, setup);
}
fn setup(mut commands: Commands) {
    let label = bind_text(|vm: &Counter| format!("Count: {}", vm.count));
    commands.spawn_scene(bsn! {
        @WingsScaffold Counter
        Children [
            @WingsColumn { @padding: 32.0, @gap: 16.0 }
            Children [
                @WingsText { @role: TypeRole::HeadlineMedium } label
                --
                @WingsButton { @text: "Increment" } IncrementButton
                on(on_activate(|vm: &mut Counter| vm.count += 1))
                --
                @WingsButton { @text: "Reset", @variant: WingsButtonVariant::Outlined }
                on(on_activate(|vm: &mut Counter| vm.count = 0))
            ]
        ]
    });
}
```

Source: https://github.com/doublegarts/bevy_wings/blob/main/examples/mvvm.rs

**Try it:** Add a decrement action that is enabled only when count is greater than zero. Trigger it from the UI and from a test.

**Check:** The label follows the component value and the disabled state follows the same model. No separate mutable display count is needed.

## 6. Put business rules in the domain

The domain crate is ordinary Rust without a Bevy dependency. It defines entities, typed identifiers, repository contracts, validation, paging and use cases. This boundary allows you to test a business operation without opening a window, and to reuse it from a server or a command-line tool.

Id<T> distinguishes identities by entity type. A user identifier cannot accidentally be passed where an order identifier is expected. DomainEntity gives a record its identity. ReadRepository and WriteRepository describe access operations without committing the caller to a database, HTTP backend or in-memory implementation.

A use case represents one meaningful operation with input and output. Collect validation failures before writing. The focused example validates a title and creates an entity with a typed ID. In an application, the screen turns a user action into a use-case call, then converts the resulting value or DomainError into presentation state.

Do not let the domain depend on widget entities or theme roles. Conversely, a view model should not need to know a database connection string or a server response’s transport fields. Dependency injection supplies the concrete repository behind the contract and keeps those decisions at the application’s composition boundary.

```rust
//! Plain Rust business rules: typed identity, validation and a use case.
use bevy_wings::prelude::*;

#[derive(Clone, Debug, PartialEq)]
struct Note {
    id: Id<Note>,
    title: String,
}
impl DomainEntity for Note {
    fn id(&self) -> Id<Note> {
        self.id
    }
}

fn main() -> DomainResult<()> {
    let validator = Validator::<String>::new()
        .rule(
            "title",
            |title| rules::not_blank(title),
            "A title is required",
        )
        .rule(
            "title",
            |title| rules::length_between(title, 1, 40),
            "Use at most 40 characters",
        );
    println!("Invalid title: {:?}", validator.errors(&" ".into()));
    let create = use_case(move |title: String| {
        validator.validate(&title)?;
        Ok(Note {
            id: Id::new(1),
            title,
        })
    });
    println!("Created: {:?}", create.execute("Ship Wings".into())?);
    Ok(())
}
```

Source: https://github.com/doublegarts/bevy_wings/blob/main/examples/domain.rs

**Try it:** Make the example reject a blank title and add another validation rule. Test a valid entity and all expected failures.

**Check:** Invalid input produces field errors without a write. Valid input returns an entity whose identity has the correct type.

## 7. Cache, work offline and synchronize

The data layer implements the domain’s access contracts. OfflineFirstRepository combines a local copy with a RemoteSource. CacheFirst returns a fresh cached record without asking the backend, refreshes expired values and can retain stale data when a remote request is unavailable. RemoteFirst, CacheOnly and RemoteOnly represent other explicit tradeoffs.

Choose a policy from the needs of the operation, not from the convenience of a global default. A read-only reference list may tolerate stale values; a transaction that requires current availability may not. Label retained values when the UI needs to distinguish cached data from a successful fresh response.

Writes are applied locally and sent to the remote. When unavailable, pending writes remain queued for sync in order. This gives a responsive application, but it does not solve every backend conflict or define a durable disk format. Implement the storage and synchronization rules appropriate to the records you own.

The focused example uses FakeRemote to demonstrate online reads, cached reads, an offline write and synchronization after reconnecting. ManualClock makes expiry tests deterministic. Mappers convert transfer data into domain entities so transport-specific formats do not leak into use cases.

```rust
//! Cached reads, offline writes and synchronization after reconnecting.
use bevy_wings::prelude::*;
use std::sync::Arc;

#[derive(Clone, Debug, PartialEq)]
struct Note {
    id: Id<Note>,
    title: String,
}
impl DomainEntity for Note {
    fn id(&self) -> Id<Note> {
        self.id
    }
}

fn main() -> DomainResult<()> {
    let remote = Arc::new(FakeRemote::with_items([Note {
        id: Id::new(1),
        title: "Read me".into(),
    }]));
    let repo = OfflineFirstRepository::new(remote.clone());
    println!("First read: {:?}", repo.all()?);
    let _ = repo.all()?;
    println!("Two reads, {} remote call", remote.calls());
    remote.set_online(false);
    repo.save(Note {
        id: Id::new(2),
        title: "Written offline".into(),
    })?;
    println!(
        "Offline: {:?}; queued writes: {}",
        repo.all()?,
        repo.pending_writes()
    );
    remote.set_online(true);
    println!(
        "Synced {} write; remote: {:?}",
        repo.sync()?,
        remote.items()
    );
    Ok(())
}
```

Source: https://github.com/doublegarts/bevy_wings/blob/main/examples/data.rs

**Try it:** Run the data example, then change its cache lifetime and offline sequence. Inspect the remote call count before and after expiration.

**Check:** Fresh cached reads avoid remote calls. Reconnection delivers the queued write and the local and remote records agree.

## 8. Load in the background without losing context

Blocking work must not run on the UI update path. Register the loadable value type, then call commands.entity(owner).load with a closure returning a domain result. Wings runs that closure on Bevy’s compute pool and records the lifecycle on WingsLoadable<T>. The entity is the owner of the visible loading state.

The example loads a String, marks it as refreshing and displays failures. load.value() can preserve the previous successful value while a new operation is running or has failed. This avoids replacing useful content with an empty screen every time a refresh occurs. The UI can show an inline progress indicator and an error alongside retained content.

Use a Changed<WingsLoadable<T>> query when rendering the lifecycle. React to the result rather than repeatedly starting new work from the render system. When a use case is supplied through dependency injection, move the resolved service into the loading closure; keep the closure’s ownership and thread-safety requirements explicit.

A background task does not make a network operation instant or guarantee success. Design the failure state, a deliberate retry action and cancellation or obsolescence handling where your workflow needs them. The focused example’s artificial delay is for observation, not a recommendation to sleep during normal work.

```rust
//! Load on a background task, keep previous data while refreshing and show failures.
mod support;
use bevy::prelude::*;
use bevy_wings::prelude::*;

#[derive(Component, Default, Clone)]
struct ResultLabel;

fn main() {
    support::run("Wings: background loading", configure);
}
fn configure(app: &mut App) {
    app.register_loadable::<String>()
        .add_systems(Startup, setup)
        .add_systems(Update, render);
}
fn setup(mut commands: Commands) {
    commands.spawn_scene(bsn! {
        @WingsScaffold
        Children [
            @WingsColumn { @padding: 32.0, @gap: 16.0 }
            Children [
                @WingsText { @text: "Background loading", @role: TypeRole::HeadlineMedium }
                --
                @WingsText { @text: "Press Load" } ResultLabel
                --
                @WingsButton { @text: "Load / refresh" }
                on(|_: On<Activate>, labels: Query<Entity, With<ResultLabel>>, mut commands: Commands| {
                    for label in &labels {
                        commands.entity(label).load(|| {
                            std::thread::sleep(std::time::Duration::from_millis(500));
                            Ok("Loaded on the compute pool".to_owned())
                        });
                    }
                })
                --
                @WingsButton { @text: "Simulate failure", @variant: WingsButtonVariant::Outlined }
                on(|_: On<Activate>, labels: Query<Entity, With<ResultLabel>>, mut commands: Commands| {
                    for label in &labels {
                        commands.entity(label).load(|| Err::<String, _>(DomainError::Unavailable("Offline".into())));
                    }
                })
            ]
        ]
    });
}
fn render(mut labels: Query<(&WingsLoadable<String>, &mut Text), Changed<WingsLoadable<String>>>) {
    for (load, mut text) in &mut labels {
        text.0 = format!(
            "{}{}{}",
            load.value().map(String::as_str).unwrap_or("No data"),
            if load.is_loading() {
                " (refreshing...)"
            } else {
                ""
            },
            load.error()
                .map(|error| format!("; {error}"))
                .unwrap_or_default(),
        );
    }
}
```

Source: https://github.com/doublegarts/bevy_wings/blob/main/examples/loading.rs

**Try it:** Load successfully, then simulate a failure. Start another refresh and inspect the label while the work is still running.

**Check:** The previous value remains visible during refresh and failure, and the lifecycle explains what is happening.

## 9. Wire implementations with dependency injection

Build services at the composition root rather than inside screen constructors. A WingsModule declares values and providers. A singleton is shared; a factory creates a new instance on resolution; a scoped provider shares within one scope. Named providers distinguish multiple implementations with the same Rust type.

Use trait bindings to provide a concrete data repository behind a domain contract. Parameterized factories accept runtime values, such as the identity of a selected record. Resolve services with WingsDi or Inject in a system. Register navigation and view-model providers only after their dependencies have definitions.

Pick a lifetime deliberately. A shared HTTP client is a reasonable singleton. Screen-specific editable state generally belongs to the navigation entry’s scope. A factory is useful when each operation needs isolated temporary state. Avoid putting per-user or per-screen mutable values into an application-wide singleton by accident.

Wings detects cycles and missing definitions and reports the dependency chain. verify() constructs definitions during a check so errors appear before a visitor opens the affected screen. Scope closure happens when its owning entity is despawned. The example makes the difference between shared, factory and scoped values observable.

```rust
//! Singleton sharing, factories, scoped services and dependency resolution.
use bevy_wings::prelude::*;
use std::sync::Arc;

struct Config {
    greeting: &'static str,
}
struct Greeting {
    config: Arc<Config>,
}
struct ScreenState;

fn main() -> Result<(), DiError> {
    let di = WingsDi::from_modules([WingsModule::new("example")
        .single(|_| Config {
            greeting: "Hello from DI",
        })
        .factory(|r| Greeting { config: r.get() })
        .scoped(|_| ScreenState)])?;
    assert!(di.verify().is_empty());
    let first = di.get::<Greeting>();
    let second = di.get::<Greeting>();
    assert!(!Arc::ptr_eq(&first, &second));
    assert!(Arc::ptr_eq(&first.config, &second.config));
    let scope = ScopeId::Id(7);
    assert!(Arc::ptr_eq(
        &di.scope(scope).get::<ScreenState>(),
        &di.scope(scope).get::<ScreenState>()
    ));
    di.close_scope(scope);
    println!(
        "{}; factories differ, singleton is shared, screen scope closed",
        first.config.greeting
    );
    Ok(())
}
```

Source: https://github.com/doublegarts/bevy_wings/blob/main/examples/di.rs

**Try it:** Resolve a singleton twice and a factory twice, then close a scope containing a scoped service. Add an intentionally missing dependency in a test.

**Check:** Shared values retain identity; factory values are distinct; a bad provider graph fails during verification rather than later in a screen.

## 10. Navigate while preserving screen state

Destinations are values of your own enum. WingsBackStack stores entries, and WingsNavigationPlugin maps a destination into a scene entry. WingsNavDisplay displays the current stack. Buttons push and pop the resource through ordinary event observers.

Entries below the top remain alive, preserving their state. Returning from a detail screen should restore the list’s model rather than construct an unrelated fresh list. Popping an entry removes that screen and closes its scoped services. Use replace when the intended transition should discard the current entry instead of adding another back step.

Adaptive panes describe their role with list_pane and detail_pane. Wide windows can show list and detail together; narrow ones show the active pane. The focused example demonstrates the same two destinations in both configurations. Escape and a visible Back action give the user predictable ways to return.

Keep navigation as presentation state. A repository should not push a destination, and a route enum should not carry a backend connection. An intent can finish a use case and then request navigation from the application layer. Test stack operations and lifecycle separately from animated transitions, which may need several frames to settle.

```rust
//! Push and pop destinations; wide windows show list and detail side by side.
mod support;
use bevy::prelude::*;
use bevy_wings::prelude::*;

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
enum Screen {
    List,
    Detail,
}
fn main() {
    support::run("Wings: navigation", configure);
}
fn configure(app: &mut App) {
    app.add_plugins(WingsNavigationPlugin::new(Screen::List, |screen: &Screen| match screen {
        Screen::List => nav_entry(bsn! {
            @WingsColumn { @padding: 32.0, @gap: 16.0 }
            Children [
                @WingsText { @text: "Your list", @role: TypeRole::HeadlineMedium }
                --
                @WingsButton { @text: "Open detail" }
                on(|_: On<Activate>, mut stack: ResMut<WingsBackStack<Screen>>| { stack.push(Screen::Detail); })
            ]
        }).list_pane(),
        Screen::Detail => nav_entry(bsn! {
            @WingsColumn { @padding: 32.0, @gap: 16.0 }
            Children [
                @WingsText { @text: "Detail pane", @role: TypeRole::HeadlineMedium }
                --
                @WingsButton { @text: "Back (or Escape)" }
                on(|_: On<Activate>, mut stack: ResMut<WingsBackStack<Screen>>| { stack.pop(); })
            ]
        }).detail_pane(),
    })).add_systems(Startup, |mut commands: Commands| {
        commands.spawn_scene(bsn! { WingsNavDisplay::<Screen> Node { width: percent(100), height: percent(100) } });
    });
}
```

Source: https://github.com/doublegarts/bevy_wings/blob/main/examples/navigation.rs

**Try it:** Open the detail screen, resize across the adaptive-pane boundary and return to the list. Add a model value to the list before navigating.

**Check:** The list value survives the round trip. The detail entity disappears after the pop transition completes.

## 11. Add purposeful motion and shader effects

Animation communicates changes rather than substituting for them. Use a tween when you want a known duration and easing curve. Use a spring when the target may change while motion is underway: the spring preserves velocity when retargeted, so repeated actions can remain continuous rather than restarting abruptly.

An animator targets a component property. The focused example uses WingsScale and a spring. WingsProperty supports additional component fields; keyframes, repeat and ping-pong cover multi-stage effects. Animated visibility can combine fade, scale and slide, ending hidden content with Display::None so it stops participating in layout.

The default shaders feature adds WESL UI materials for ripples, shimmer skeletons, circular progress, aurora backgrounds and glass panels. Those materials require the rendering stack. Without shaders, skeleton feedback has a simpler fallback and shader-dependent widgets are unavailable. Keep state and interactions usable with either configuration.

Test retargeting while an animation is already running and inspect the final value. Verify appearance in a real window: a headless material construction test is not a visual test. Motion should support focus, selection and loading feedback, while leaving users able to understand the interface without waiting for an effect to finish.

```rust
//! Click to retarget a spring while preserving its velocity.
mod support;
use bevy::prelude::*;
use bevy_wings::prelude::*;

#[derive(Component, Default, Clone)]
struct AnimatedCard;
fn main() {
    support::run("Wings: animation", configure);
}
fn configure(app: &mut App) {
    app.add_systems(Startup, setup);
}
fn setup(mut commands: Commands) {
    commands.spawn_scene(bsn! {
        @WingsScaffold
        Children [
            @WingsColumn { @padding: 48.0, @gap: 32.0 }
            Children [
                @WingsText { @text: "Spring retargeting", @role: TypeRole::HeadlineMedium }
                --
                @WingsCard AnimatedCard UiTransform
                Children [ @WingsText { @text: "I keep my velocity when you click again." } ]
                --
                @WingsButton { @text: "Toggle scale" }
                on(|_: On<Activate>, mut commands: Commands, cards: Query<(Entity, &UiTransform), With<AnimatedCard>>| {
                    for (entity, transform) in &cards {
                        let scale = if transform.scale.x > 1.1 { 1.0 } else { 1.25 };
                        commands.entity(entity).animate::<ScaleProperty>(Vec2::splat(scale), AnimationSpec::spring());
                    }
                })
            ]
        ]
    });
}
```

Source: https://github.com/doublegarts/bevy_wings/blob/main/examples/animation.rs

**Try it:** Toggle the animation target while the card is moving. Compare a shader-enabled run with a no-default-features run of a compatible example.

**Check:** Motion retargets continuously and the interface still communicates its state when decorative shader effects are absent.

## 12. Test behavior and ship a reproducible application

Build tests around behavior visible at the framework boundary. A domain test asserts validation and repository outcomes. A data test controls time and remote availability. A UI test triggers an event, advances the app and inspects components. This is more useful than asserting that a function called another function with a particular internal arrangement.

The focused examples contain small integration tests next to the runnable code. The MVVM test triggers Activate and checks the text. The navigation test pushes and pops a destination and checks which labels remain. The loading test waits through a bounded sequence of updates for the result to appear. Use those examples as starting points for the workflows you add.

Wings systems run through ordered sets: Input, ViewModel, Bind, Navigation, Responsive, Modifier, Style, Theme and Animate. Put application systems that mutate view models in the intended stage so bindings can observe them in the same frame. If a value appears one frame late, examine ordering before adding another copy of the state.

Run formatting, Clippy, unit and integration tests, examples and documentation tests for the configurations you support. Cargo --all-targets does not include documentation tests, so run them separately. Keep Cargo.lock for reproducible --locked checks. A release also needs real-window input checks, narrow and wide layouts, asset paths, error states and keyboard navigation.

The complete gallery joins the layers: the screen resolves a use case, the use case depends on a domain contract, a data implementation serves the request and the result becomes view-model state. Follow that path when debugging. First identify the failing boundary, then write a small test there. Add infrastructure only when an actual workflow needs it.

```sh
cargo fmt --all -- --check
cargo test -p bevy_wings --examples --all-features --locked
cargo test --workspace --doc --all-features --locked
```

**Try it:** Run the example tests below and inspect one test in examples/mvvm.rs. Extend it with a reset assertion after incrementing.

**Check:** The extended test proves that the user intent changes the model and the bound text follows it. A browser or window check confirms the visual presentation separately.


## Package reference

1. `bevy_wings`
2. `bevy_wings_domain`
3. `bevy_wings_data`
4. `bevy_wings_di`
5. `bevy_wings_ui_core`
6. `bevy_wings_ui_animation`
7. `bevy_wings_ui_theme`
8. `bevy_wings_ui_responsive`
9. `bevy_wings_ui_modifiers`
10. `bevy_wings_ui_shaders`
11. `bevy_wings_ui_widgets`
12. `bevy_wings_ui_mvvm`
13. `bevy_wings_ui_navigation`
