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.
Mark descendants you want to bounce-in with the enter-stagger USS class:
<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).
UIViewComponent — designer-driven, no codeFor views attached to a UIViewComponent, add a transition entry directly in the inspector:
| Field | Value |
|---|---|
| Animation | (leave empty — the bounce preset is used automatically when stagger is set) |
| Element Name | empty (= the UIView's parent element) or a child name to scope to a subtree |
| Trigger | FadeInStart |
| Stagger Selector | enter-stagger |
| Stagger Step Ms | 80 (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.
Animationis ignored whenStaggerSelectoris set — the canonical bounce preset is used to keep stagger entries visually consistent across consumers. SetStaggerSelector = ""if you want to play a custom Animation on a single target instead.
For views without a UIViewComponent (e.g. screens mounted by LunaNavigationHost), call LunaUITransitions.PlayStaggeredEntry(root) at the end of your Mount method:
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.
PlayBounceOnWhen 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):
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-inLuna's TabView integrates with the convention via EnableStaggerEntry():
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.
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;
}
}Built once at class init and shared across calls. Equivalent to:
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.
EnableStaggerEntry()Runtime/Scripts/TransitionAnimation/LunaUITransitions.csSettings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction