Stagger entry is the "DOM-order bounce-in on screen entry" pattern every game-style UI reaches for. Luna has no separate stagger system — it's just a UIViewTransitionEntry whose Target is an ElementSelector matching multiple elements with a non-zero StepMs between them. The same machinery handles single-target animations and multi-target stagger passes.
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>enter-stagger is convention, not a hard-coded selector — any USS class works. The class only means something because your transition entry's Target selector names it.
UIViewComponent — designer-driven, no codeOn any view backed by a UIViewComponent, add one entry to the inspector _transitions list:
| Field | Value |
|---|---|
| Animation | A TransitionSequenceAsset containing the bounce-in sequence (opacity 0→1 / scale 0.85→1, or any preset you like) |
| Target | Class Name = enter-stagger |
| Trigger | FadeInStart |
| StepMs | 80 (delay between successive matches) |
When the view fades in, every descendant matching the class plays the animation in DOM order, 80 ms apart. No code at the call site. Re-fires on every push — OnFadeInStart triggers it.
UIViewTransitionEntry is just a serializable POCO — same in code:
myUIView.AddTransition(new UIViewTransitionEntry
{
Animation = bounceAsset,
Target = new ElementSelector { ClassName = "enter-stagger" },
Trigger = FadeEventTrigger.FadeInStart,
StepMs = 80L,
});When you need ordering or per-element delays that DOM order can't express — e.g. a "3rd → 2nd → 1st" podium reveal where the DOM is positional rather than ranked — play your bounce TransitionSequenceAsset directly on each element with a scheduled delay:
[SerializeField] TransitionSequenceAsset _entryBounce;
void PlayPodiumReveal(VisualElement body) {
PlayOn(body.Q<VisualElement>("Podium3"), 0L);
PlayOn(body.Q<VisualElement>("Podium2"), 100L);
PlayOn(body.Q<VisualElement>("Podium1"), 200L);
}
void PlayOn(VisualElement target, long delayMs) {
if (target == null) return;
var player = _entryBounce.CreatePlayer();
if (delayMs <= 0) player.Play(target);
else target.schedule.Execute(() => player.Play(target)).ExecuteLater(delayMs);
}If DOM order happens to match the desired reveal order, the inspector path (multi-match Target + StepMs) is simpler — only reach for explicit code timing when DOM and reveal disagree.
Build a TransitionSequenceAsset once and drop it in every screen's transition entry. The Luna samples ship one; the equivalent code is:
new TransitionSequence()
.Together(g => g
.Opacity(1f, 0.32f, EasingMode.EaseOutCubic)
.Scale(1f, 0.36f, EasingMode.EaseOutBack))
.ClearInlineStyles(StyleProperty.Opacity, StyleProperty.Scale)
.Build();For matched elements, set inline initial state at the start of the sequence (opacity = 0, scale = 0.85) — or rely on a USS rule scoped to .enter-stagger that does the same.
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.
namespace CupkekGames.Luna
{
[Serializable]
public class UIViewTransitionEntry
{
public TransitionSequenceAsset Animation;
public ElementSelector Target; // default = no filters (the view root)
public FadeEventTrigger Trigger; // default = FadeInStart
public long StepMs; // default = 0
}
[Serializable]
public class ElementSelector
{
public enum Scope { Self, Children, Descendants }
public enum ElementType { Any, Label, Button, TextField, Toggle, Slider, ScrollView, Image }
public Scope ScopeValue { get; set; } // default = Self
public ElementType ElementTypeValue { get; set; } // default = Any
public string Name { get; set; } // exact name match; empty = no filter
public string ClassName { get; set; } // USS class match; empty = no filter
}
}Runtime/Scripts/UIView/UIViewTransitionEntry.cs, Runtime/Scripts/ElementSelector/ElementSelector.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