Attach on a GameObject with a PanelRenderer component (or under one — see auto-fetch below). Creates a UIView for the panel's visual tree.
PanelRenderer delivers its visual tree asynchronously via a reload callback — there is no root element to grab in Awake(). UIViewComponent registers that callback for you in its Awake() and constructs the UIView when the first reload fires. Until then, UIView (and everything that forwards to it: ParentElement, Fade, LunaUIManager) is null.

| Field | Type | Description |
|---|---|---|
| Panel Renderer | PanelRenderer | PanelRenderer hosting this view. Leave empty for the common cases — see auto-fetch. |
| Parent Name | string | Optional name of a child element to treat as this view's root. Empty = auto-resolve — see view root resolution. |
| Focus Name | string | Element to focus when this UIView becomes visible. Empty = the view root itself. |
| Fade Duration | float | Duration of the fade transition, in seconds. Default 0.5. |
| Fade In Delay | float | Delay before the fade-in animation starts, in seconds. Default 0. |
| Fade Out Delay | float | Delay before the fade-out animation starts, in seconds. Default 0. |
| Easing Mode | EasingMode | Easing applied during fade transitions. Default EaseOutCirc. |
| Transition Animations | List<UIViewTransitionEntry> | Per-fade-event animations. Each entry targets one or more elements via ElementSelector and plays its TransitionSequenceAsset with optional StepMs between matches. See UIView — Transition Animations. |
| Debug | bool | Logs PanelRenderer auto-fetch resolution at Awake, plus fade transitions and enable/disable events. |
⚠️ Query in
OnUILoaded, notAwake. The visual tree arrives asynchronously, so element queries can't live inAwake()/OnEnable()— the root isn't there yet. Use one of the members below instead.
Use one of these:
| Member | Description |
|---|---|
bool IsUILoaded | True once the first reload callback has fired and UIView has been constructed. |
event Action UILoaded | Fires exactly once, when UIView becomes available. |
void WhenUILoaded(Action action) | Runs action immediately if the UI is already loaded, otherwise on the next UILoaded emission. Saves the "if/else subscribe" pattern at every call site. |
protected virtual void OnUILoaded(VisualElement root) | Subclass hook, fired exactly once on the first reload, after UIView is constructed and the start-visibility snap is applied. root is the view's mount root (UIView.ParentElement), so queries scope to the view's own subtree. |
protected virtual void OnViewRootChanged(VisualElement root) | Subclass hook, fired on subsequent reloads whose resolved view root differs from the current one — i.e. the tree was rebuilt (in-play UXML live reload). The base has already re-parented the UIView and re-applied styling; override this to re-query elements and re-attach handlers, because everything wired in OnUILoaded died with the old tree. Default is a no-op. |
Subclass pattern — wire in OnUILoaded, re-wire in OnViewRootChanged:
public class MyView : UIViewComponent
{
private Button _button;
protected override void OnUILoaded(VisualElement root) => Wire(root);
protected override void OnViewRootChanged(VisualElement root) => Wire(root);
private void Wire(VisualElement root)
{
_button = root.Q<Button>("my-button");
_button.clicked += OnClick; // fresh element each tree — no -= needed
}
}Skipping OnViewRootChanged is fine for production views (player builds never live-reload UXML) — but with it, your view also survives UXML hot-editing in Play Mode. Keep state in fields (it survives the rewire) and keep Wire scoped to tree-bound things; subscriptions to persistent objects (game data, managers) belong in OnEnable/OnDisable with the unsubscribe-before-subscribe idiom instead.
From outside the component — WhenUILoaded:
myView.WhenUILoaded(() =>
{
myView.UIView.AddAction(new UIViewActionEscape(Close));
});Note: when the PanelRenderer is already mounted at the time the component awakes (e.g. a prefab instantiated under a shell whose panel has loaded), the reload callback — and therefore
OnUILoaded— fires synchronously duringAwake(). Code should not rely on either timing;WhenUILoaded/OnUILoadedhandle both.
Leave the Panel Renderer field empty for the common cases:
PanelRenderer on the same GameObject.PanelRenderer on a parent GameObject (the shell). Pair with Parent Name to point at a named slot in the shell's UXML.At Awake() the component walks its own GameObject and then every parent until it finds a PanelRenderer. Set the field explicitly only when neither the view's own GameObject nor any parent has the PanelRenderer you want. Enable Debug to log how the auto-fetch resolved.
The Parent Name field decides which VisualElement becomes the view's root (UIView.ParentElement):
VisualElement. The wrapper itself is unsafe to animate against — PanelSettings shuffles it during first-attach, which kills any in-progress CSS transition.⚠️ Multi-root UXML. If your UXML has more than one top-level element, the auto-fallback picks the first one and logs a warning — fade/transition wiring won't touch the other siblings. Set Parent Name explicitly, or wrap the UXML in a single outer
VisualElement.
When the view is a navigation destination nested under a parent destination, the name lookup is scoped to the parent view's subtree — so two sibling tabs can each have their own "DetailContainer" without cross-matching.
| Name | Type | Description |
|---|---|---|
| UIView | UIView | The UIView constructed at the first PanelRenderer reload. null until then. |
| PanelRenderer | PanelRenderer | The PanelRenderer hosting this view (assigned or auto-fetched). |
| ParentElement | VisualElement | The view's root element. Forwards to UIView.ParentElement; null before the first reload. |
| LunaUIManager | Luna UI Manager | Forwards to UIView.LunaUIManager; null before the first reload. |
| Fade | FadeUIElement | Manage fade transitions of the UIView. Forwards to UIView.Fade; null before the first reload. |
| IsUILoaded | bool | True once UIView has been constructed. |
| PushArgs | object | Per-push args set by navigation right before fade-in. See Push args. |
| Node | NavNode | The navigation node this view represents, or null when the view isn't a nav destination. See Navigation integration. |
| IsLayerPreloaded | bool | True for the single Layer-preloaded instance per node; false for multi-instance copies spawned by nav's Push path. Set by NavHost — do not mutate from consumer code. |
Start the fade-in animation. When called before UIView exists (a nav Push can land before the PanelRenderer's first reload), the fade-in is queued and runs as soon as the tree arrives.
public void Show()Start the fade-out animation. The fade pipeline applies display: none + opacity: 0 at the element level; the GameObject stays active. No-op when UIView isn't constructed yet.
public void Hide()Run an action as soon as the UI is loaded — synchronously if it already is.
public void WhenUILoaded(Action action)When a view is opened through navigation, Push(id, args) hands the view a per-push args object right before fade-in:
public object PushArgs { get; } // null when pushed without args
public T GetArgs<T>() where T : class // typed read; null on mismatch
public void SetPushArgs(object args) // called by nav; also usable from test/storybook codeThe canonical owner of the args is UIView.PushArgs; the component forwards to it, and stashes args set before the first reload in a pending slot that drains into the UIView when it's constructed. Read args in your OnFadeInStart-driven logic or in OnUILoaded:
protected override void OnUILoaded(VisualElement root)
{
var args = GetArgs<MyDialogArgs>();
}Destinations authored with ResetStateOnReopen = true ask the view to clear and re-seed its state before each reopen. Override the hook:
protected virtual void OnStateReset(object args)args is the new push args for the upcoming push. First-time push doesn't fire this — the view's state is fresh by construction.
UIViewComponent implements INavView, so it can be a navigation destination. The link is runtime-only — whoever spawns the view (the layer host, or your own code) calls:
public void SetNode(NavNode node)right after Instantiate. Setting a node registers the view with the nav system, applies node-authored state (StartVisible, DisableOtherViewsOnFadeIn, backdrop), and wires external-close detection so a direct Hide() still notifies nav. Passing null clears the node and unregisters.
Views without a node (shells, HUD overlays, UIPrefabLoader-spawned ephemerals) default to born visible and don't block input on other views.
See Navigation for the node graph, layers, channels, and push/pop semantics — this page only covers the view-side surface.
UIView is not built here.UIView, applies fade config + transitions, applies start visibility, then calls OnUILoaded(root) and raises UILoaded.UIView (UIView.SetParentElement), re-applies styling, then calls OnViewRootChanged(root) so subclasses can re-wire against the fresh tree.UIView (which releases page registrations, input-block state, and the global registry entry).Subclassing note:
AwakeandOnDestroyareprotected virtual— callbase.Awake()/base.OnDestroy()when you override them. The component deliberately does not useOnEnable/OnDisablefor its own registration, so your subclass is free to declare them.
Not everything on a panel is a view. HUD binders, tooltip hosts, and panel hooks just need elements from the tree and an event subscription — no fades, no navigation, no UIView. For those, subclass PanelRendererBinder instead: it owns the same async-delivery timing (register in Awake, wait for the first reload, retro-fire enable logic if the tree arrived late) and exposes it as three hooks:
[RequireComponent(typeof(PanelRenderer))]
public class GoldChipBinder : PanelRendererBinder
{
private Label _gold;
protected override void OnPanelReady(VisualElement root) // fires once, tree guaranteed live
=> _gold = root.Q<Label>("GoldAmount");
protected override void OnPanelEnable() // never fires before OnPanelReady
=> _wallet.OnChanged += OnWalletChanged;
protected override void OnPanelDisable()
=> _wallet.OnChanged -= OnWalletChanged;
}| Member | Description |
|---|---|
OnPanelReady(root) | Abstract; fires exactly once, on the first tree delivery. Do element queries here. |
OnPanelEnable() / OnPanelDisable() | Subscribe / unsubscribe here. Guaranteed to fire only while the tree exists, as a balanced pair — including the retro-fire when the tree arrives after Unity's OnEnable already ran. |
OnPanelReload(root, version) | Optional; fires on every delivery (editor live-reload re-init). |
PanelRoot / IsPanelReady | The delivered root; null / false until the first delivery. |
⚠️ Do not declare
OnEnable/OnDisablein aPanelRendererBindersubclass — Unity dispatches magic messages to the most-derived declaration only, which would silently disconnect the base. Use theOnPanelEnable/OnPanelDisablehooks;Awake/OnDestroyare virtual with abasecall.
The PanelRenderer is auto-fetched from the same GameObject only (no parent walk — pair with [RequireComponent(typeof(PanelRenderer))]), or wire it explicitly in the inspector. Luna's own TooltipController is built on this base.
Rule of thumb: something the player opens/closes (fade, focus, nav destination) → UIViewComponent. Something that rides along on an existing panel and binds data → PanelRendererBinder.
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