Luna ships a canonical "bounce-in" entry preset and a DOM-order stagger pass that every game-style UI reaches for on screen entry. Three call patterns serve different consumer styles — pick whichever fits where you are.

The convention

Mark descendants you want to bounce-in with the enter-stagger USS class:

xml
<ui:VisualElement class="screen-root"> <ui:Label class="enter-stagger" text="Welcome" /> <ui:VisualElement class="enter-stagger menu-buttons"> <ui:Button class="enter-stagger" text="Continue" /> <ui:Button class="enter-stagger" text="New Game" /> <ui:Button class="enter-stagger" text="Settings" /> </ui:VisualElement> </ui:VisualElement>

Then trigger the pass at the right moment using one of the three APIs below. Each enter-stagger element bounces in DOM order, 80 ms apart, with a scale 0.85 → 1 + opacity 0 → 1 curve (EaseOutCubic + EaseOutBack).

Three call patterns

1. Author on a UIViewComponent — designer-driven, no code

For views attached to a UIViewComponent, add a transition entry directly in the inspector:

FieldValue
Animation(leave empty — the bounce preset is used automatically when stagger is set)
Element Nameempty (= the UIView's parent element) or a child name to scope to a subtree
TriggerFadeInStart
Stagger Selectorenter-stagger
Stagger Step Ms80 (or whatever your view needs)

When the view fades in, every descendant matching enter-stagger bounces in DOM order. No code at the call site.

Animation is ignored when StaggerSelector is set — the canonical bounce preset is used to keep stagger entries visually consistent across consumers. Set StaggerSelector = "" if you want to play a custom Animation on a single target instead.

2. From code — explicit call after Mount

For views without a UIViewComponent (e.g. screens mounted by LunaNavigationHost), call LunaUITransitions.PlayStaggeredEntry(root) at the end of your Mount method:

csharp
public void OnEnter(VisualElement root, MyContext context, LunaNavigationHost host) { // Wire UXML to context first root.Q<Label>("Title").text = context.Title; root.Q<Button>("Confirm").clicked += context.OnConfirm; // Bounce-in everything marked enter-stagger LunaUITransitions.PlayStaggeredEntry(root); }

The Showcase sample's ShowcaseDetailScreen<TContext> base class uses this pattern — it runs PlayStaggeredEntry automatically after each screen's OnMount so subclasses don't have to remember.

3. Per-element timing — PlayBounceOn

When you need custom ordering or per-element delays that DOM order can't express (e.g. LeaderboardTab's 3rd → 2nd → 1st podium reveal where the DOM is positional, not ranked):

csharp
LunaUITransitions.PlayBounceOn(body.Q<VisualElement>("Podium3"), 0L); LunaUITransitions.PlayBounceOn(body.Q<VisualElement>("Podium2"), 100L); LunaUITransitions.PlayBounceOn(body.Q<VisualElement>("Podium1"), 200L);

Each PlayBounceOn schedules the bounce after the specified delay. Safe on null inputs.

LunaTabHost opt-in

Luna's TabView integrates with the convention via EnableStaggerEntry():

csharp
var tabView = body.Q<TabView>("MyTabs"); tabView.EnableStaggerEntry();

After this call, on each Setup / tab switch the TabView marks its header strip and the active tab's first ScrollView with the enter-stagger class. Inactive tabs stay un-marked (marking them all would freeze switched-to tabs at opacity: 0 until their next entry pass).

Combined with pattern #2 (code-side PlayStaggeredEntry on the screen root) or pattern #1 (a UIViewComponent transition entry), the TabView's header + active body bounce in DOM order alongside the rest of the screen.

Public API reference

csharp
namespace CupkekGames.Luna { public static class LunaUITransitions { public const string DefaultStaggerSelector = "enter-stagger"; public const long DefaultStaggerStepMs = 80L; public static void PlayBounceOn(VisualElement item, long delayMs = 0L); public static void PlayStaggeredEntry( VisualElement root, string staggerSelector = DefaultStaggerSelector, long stepMs = DefaultStaggerStepMs); } [Serializable] public class UIViewTransitionEntry { public TransitionSequenceAsset Animation; // ignored when StaggerSelector is set public string ElementName = ""; public FadeEventTrigger Trigger = FadeEventTrigger.FadeInStart; public string StaggerSelector = ""; // e.g. "enter-stagger" public long StaggerStepMs = 80L; } }

The bounce preset

Built once at class init and shared across calls. Equivalent to:

csharp
new TransitionSequence() .Together(g => g .Opacity(1f, 0.32f, EasingMode.EaseOutCubic) .Scale(1f, 0.36f, EasingMode.EaseOutBack)) .ClearInlineStyles(StyleProperty.Opacity, StyleProperty.Scale) .Build();

Before play, PlayBounceOn sets inline initial state: opacity = 0, scale = new Scale(new Vector3(0.85f, 0.85f, 1f)). The sequence animates to (opacity 1, scale 1) and clears the inline styles on completion so any USS-driven state takes over.

Translate is deliberately not animated. Some marked elements carry USS-declared translates for centering (translate: -50% 0), and setting inline translate at entry overrides those, snapping the element off-anchor for the duration. Scale + opacity produces a sufficient grow-in feel without the conflict.

See also

  • Transition Animation — the builder API stagger entry runs under the hood
  • UIViewComponent — author transition entries in the inspector
  • Tabview — opt-in via EnableStaggerEntry()
  • Source: Runtime/Scripts/TransitionAnimation/LunaUITransitions.cs

Settings

Theme

Light

Contrast

Material

Dark

Dim

Material Dark

System

Sidebar(Light & Contrast only)

Light
Dark

Font Family

DM Sans

Wix

Inclusive Sans

AR One Sans

Direction

LTR
RTL