In my opinion, Bevy’s UI solution has long been a point of struggle. UI definitions are written in compiled Rust, which means you have to go through a compile cycle each time you intend to test a UI change. This is unconventional for UI solutions, which typically allow you to hot-reload (JS/TS) or at least avoid compilation cycles to test changes, only requiring reloads (Unity as I remember it)

However, bevy_cobweb_ui is a crate that allows you to specify UI in a format called .cob which turns into standard Bevy UI under the hood at runtime, and is even hot reloadable. It also inherits an animation (aka easing) system from an older Bevy UI solution called sickle_ui. Being able to specify these animations in .cob is a step even above standard Unity.

In my Bevy game Mercator I make heavy use of this system. You can download the demo right now if you wish to see the UI in action. The .cob files involved in the UI are also readily available from the assets folder attached to the zip file.

Below I’ve described a few UI elements and how I made them:

  1. Image enlargement and rotation

Screen Recording 2025-03-15 at 2.40.16 PM.mov

I was trying to go for an effect that makes it easier to identify which “item box” you are pointing your cursor at. Here I make use of two tricks: pseudo-state switching and transform rotation.

  1. Psuedo-state switching

From assets/item_box.cob:

Multi<Animated<Height>>[
        { 
            state:[Disabled]
            idle:$idle_size
            hover:$idle_size
            hover_with:{ duration:$duration ease:OutCubic delay:$delay}
            unhover_with:{ duration:$duration ease:OutCubic delay:$delay}
        },
        { 
            state:[Checked]
            idle:$idle_size
            hover:$hover_size
            hover_with:{ duration:$duration ease:OutCubic delay:$delay}
            unhover_with:{ duration:$duration ease:OutCubic delay:$delay}
        }
    ]

Step by step on what this code represents:

b. Transform rotation

Rotation (to my knowledge) can’t be easily manipulated this way, instead you’ll need to use the fact that Cobweb UI is just Bevy UI under the hood:

pub fn item_box_hover_cosmetic<'a>(l: &mut LoadedScene<'a, UiBuilder<'a, Entity>>) {
    let e_id = l.id();
    l.on_pointer_enter(
        move |mut c: Commands, mut t_q: Query<&mut Transform, With<ItemBox>>| {
            let Ok(mut t) = t_q.get_mut(e_id) else {
                return;
            };
            bounce_box(&mut t);
            c.trigger(SpawnSound::new("music/hover.ogg"));
        },
    );
}

pub fn bounce_box_factor(t: &mut Transform, factor: f32) {
    t.rotation =
        Quat::from_rotation_z(factor * (0.4 * if fastrand::bool() { -1. } else { 1. }) * PI / 2.);
}