Skip to content

Animation guide

This guide presents an end-to-end workflow for adding animations to a multi-widget screen. It builds on the Animation concept page for the model and the Add an animation task for the single-widget recipe. Read both pages first.

What this guide builds

A small settings screen with three animated parts:

  • A panel that fades in when shown and fades out when hidden.
  • A list of toggle rows, each with a hover-driven background tint.
  • A confirm button at the bottom that animates colour and scale on hover.

Each animated part demonstrates a different driver: panel visibility (via BoolWhileTrue / BoolWhileFalse) for the fade, and the built-in Enter / Leave hover transitions for the rows and the button.

Step 1: panel show and hide animation

BeginPanel returns true while the panel is visible or animating out (Hiding). Open a PushAnimate scope inside the body to drive the slide-and-fade against the panel's visibility:

if (ui.BeginPanel("settings", screenRect, new PanelConfig
{
    Hidden = true,
    BackdropColor = new Color(0, 0, 0, 0.5f),
}))
{
    var ctx = ui.PushAnimate("settings_anim")
        .Init().Color().Set(Color.clear).Done()
        .BoolWhileTrue("shown", ui.IsPanelVisible("settings"))
            .Color().Blend(Color.clear, panelColor, 0.25f)
            .Done()
        .BoolWhileFalse("hidden", ui.IsPanelVisible("settings"))
            .Color().Blend(panelColor, Color.clear, 0.25f)
            .Done()
        .Apply();

    // ... centred body, can use ctx.GetColor() for the panel chrome ...

    ui.PopAnimate();
    ui.EndPanel();
}

// Show with ShowModal (not ShowPanel) to block input below.
if (m_userOpenedSettings)
    ui.ShowModal("settings");

Calling HidePanel("settings") flips IsPanelVisible to false, which transitions the context to its fade-out blend. The panel spends one frame in Hiding (its body still runs, but it no longer accepts input) and then EndPanel moves it to Hidden. The BoolWhileFalse blend continues to evaluate on the frames the panel is shown again, so the fade reads correctly the next time the panel opens; the framework does not hold the panel open for the blend's full duration.

For the visibility state machine, refer to Panels and modals.

Step 2: per-row hover animation inside a loop

Each row in the toggle list needs its own animation context. PushID(row.Id) per iteration scopes the context's ID so each row's state stays separate:

foreach (var row in m_settings.Rows)
{
    ui.PushID(row.Id);

    using (ui.Auto().Top(48))
    {
        var ctx = ui.PushAnimate("row_anim")
            .Init().Color().Set(rowRest).Done()
            .Enter().Color().Blend(rowRest, rowHover, 0.15f).Done()
            .Leave().Color().Blend(rowHover, rowRest, 0.15f).Done()
            .Apply();

        ui.FillRounded(ctx.GetColor(), 4);
        ui.Text(row.Label, "roboto_mono", 14, textColor,
            TextAlign.MiddleLeft);

        if (ui.Button("toggle"))
            row.Value = !row.Value;

        ui.PopAnimate();
    }

    ui.PopID();
}

PushID ensures each row's "row_anim" context is keyed by a distinct combined ID. Without it, every row would share one context.

For the identity model, refer to IDs.

Step 3: hover feedback on the confirm button

The confirm button animates colour and scale together on hover. .Enter() and .Leave() are driven by the widget's hover state, and .Apply() pushes the animated scale automatically, so the content drawn before PopAnimate is already scaled:

using (ui.Auto().Bottom(48).PadHorizontal(20))
{
    var ctx = ui.PushAnimate("confirm")
        .Init().Color().Set(buttonRest).Scale().Set(1.0f).Done()
        .Enter().Color().Blend(buttonRest, buttonHover, 0.15f)
                .Scale().Blend(1.0f, 1.03f, 0.15f).Done()
        .Leave().Color().Blend(buttonHover, buttonRest, 0.15f)
                .Scale().Blend(1.03f, 1.0f, 0.15f).Done()
        .Apply();

    ui.FillRounded(ctx.GetColor(), 5);
    ui.Text("Apply", "roboto_mono", 14, Color.white,
        TextAlign.MiddleCenter);

    if (ui.Button("apply"))
        ApplySettings();

    ui.PopAnimate();
}

Performance budget

Each PushAnimate context allocates an entry on the animation stack and reads its values per frame. The cost is small per context but multiplies across a list of dozens of animated rows.

Two optimisations:

  • Share a single context across visually identical widgets when possible, instead of opening one per row.
  • Disable animation per-row when the row is off-screen (a scroll view culls these naturally; a long flat list does not).

For the broader profiling workflow, refer to the Performance guide.

Additional resources