You can find this example in the Showcase sample — open LunaStorybook.unity and pick the MultiPageUI entry. The source lives at Showcase/Storybook/Entries/MultiPageUI/MultiPageUIDemoController.cs.
The controller is a UIViewComponent whose UXML contains two page containers (Page1, Page2) plus Prev/Next buttons. It builds one extra UIView per page by hand — a single panel hosting several conceptually independent sub-views, with one sub-view "active" at a time:
using UnityEngine.UIElements;
namespace CupkekGames.Luna.Sample.Showcase
{
/// <summary>
/// Storybook entry for MultiPageUI — nav-driven UIViewComponent in Inject mode.
/// The multi-page content is the entry body (Page1 + Page2 + Prev/Next buttons),
/// inlined from the former MultiPageUIModal prefab. Page1 starts visible; Next
/// fades Page2 in over Page1; Prev (or Esc) on Page2 returns to Page1.
/// Prev is hidden on Page1 — there's no "modal" to close in the storybook context.
/// </summary>
public class MultiPageUIDemoController : UIViewComponent
{
UIView _pageFirst;
UIView _pageSecond;
Button _next;
Button _previous;
protected override void Awake()
{
base.Awake();
_pageFirst = new UIView(gameObject, ParentElement.Q<VisualElement>("Page1"));
_pageFirst.ApplyStartVisibility(true);
_pageSecond = new UIView(gameObject, ParentElement.Q<VisualElement>("Page2"));
_pageSecond.ApplyStartVisibility(false);
// Esc on Page2 returns to Page1; no Esc action on Page1 (it's the demo root).
_pageSecond.AddAction(new UIViewActionEscape(() => GoToPage(0)));
UIView.AddChild(_pageFirst);
_next = ParentElement.Q<Button>("Next");
_previous = ParentElement.Q<Button>("Prev");
UpdateButtonsForPage(0);
}
void OnEnable()
{
if (_previous != null) _previous.clicked += OnPrevious;
if (_next != null) _next.clicked += OnNext;
}
void OnDisable()
{
if (_previous != null) _previous.clicked -= OnPrevious;
if (_next != null) _next.clicked -= OnNext;
}
void OnNext()
{
if (_pageSecond != null && !_pageSecond.IsVisible) GoToPage(1);
}
void OnPrevious()
{
// Page1: no-op (no modal to close in storybook context).
// Page2: back to Page1.
if (_pageSecond != null && _pageSecond.IsVisible) GoToPage(0);
}
void GoToPage(int index)
{
if (index == 0)
{
_pageFirst.Fade.FadeIn();
_pageSecond.Fade.FadeOut();
UpdateButtonsForPage(0);
}
else if (index == 1)
{
_pageFirst.Fade.FadeOut();
_pageSecond.Fade.FadeIn();
UpdateButtonsForPage(1);
}
}
void UpdateButtonsForPage(int index)
{
if (_previous != null)
{
_previous.style.display = index == 0 ? DisplayStyle.None : DisplayStyle.Flex;
_previous.text = "Previous";
}
if (_next != null)
{
_next.style.display = index == 0 ? DisplayStyle.Flex : DisplayStyle.None;
}
}
}
}Things worth noticing:
UIView instances constructed with just (gameObject, element) — page-level fades reuse the same fade pipeline as full views.ApplyStartVisibility — a pure snap, no animation: Page1 starts visible, Page2 hidden. From then on, visibility changes go through Fade.FadeIn() / Fade.FadeOut().Awake here because this storybook entry is spawned under a shell whose PanelRenderer is already mounted, so the visual tree is delivered synchronously during base.Awake() and ParentElement is immediately available. A standalone prefab can't rely on that — do the queries in OnUILoaded(root) instead (see UIViewComponent).UIViewActionEscape on Page2 registers while Page2 is visible and unregisters when it fades out, so Esc means "back to Page1" only on the second page.I will elaborate on the problem and solution by building upon the example provided above.
// Add a UIViewActionEscape with Debug.Log to _pageFirst
// Set the second parameter, bool activate, to false because we don't want the Action to register immediately.
// _pageFirst was just snapped visible by ApplyStartVisibility(true), but the parent view
// (this component's UIView) may itself still be hidden at this point.
// Otherwise, this Action would register at startup while the whole demo view is closed.
_pageFirst.AddAction(new UIViewActionEscape(() => Debug.Log("First page: Can't escape me!")), false);Actions such as UIViewActionEscape are registered when a UIView fades in. However, if you control the visibility of a UIView through a parent UIView, the actions of the child UIView will not be registered.
In this case, _pageFirst is never faded directly — it's snapped visible at build time, and it only appears or disappears because the whole demo view fades in or out. This means the Action we added to _pageFirst would never activate.
(_pageSecond doesn't have this problem: GoToPage fades it directly, so its Esc action registers and unregisters on its own fade events.)
To fix this issue, simply use the AddChild function — the sample already does this:
UIView.AddChild(_pageFirst);With this, the Actions of the child _pageFirst register/unregister in sync with the parent. When the demo view fades in, the child's currently-visible state is re-applied — the UIViewActionEscape with the debug message registers, and the next escape input prints the message to the console. Note the release direction is not symmetric: on parent fade-out, only currently-hidden children get their actions' OnFadeOut re-fired — a still-visible child like _pageFirst keeps its action registered until the child itself fades out, so an action that must not outlive the parent should be released explicitly (or attached to the parent view instead).
Settings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction