Assumes you've finished Quick Start (you know the Panel Settings → TSS chain) and know what UXML, USS, and a VisualElement are (covered in UI Toolkit Basics).
This walkthrough does one thing on purpose: shows that Luna's theme applies itself to standard UITK controls the moment you point a PanelRenderer at LunaPanelSettings. The end state is a brand-new scene with a Luna-themed label + button you wired from C# — no extra USS, no extra prefab, just standard <Label> and <Button> picking up Luna's look automatically.
Before you build from scratch: Luna ships ready-made Views — Main Menu, Pause, Settings, Save & Load, Credits, Inventory, Dialogue. For full-screen flows, extending one of those is usually faster than authoring your own from zero. This page is the canonical "how do I write a custom view of my own?" walkthrough — useful either way, since extending a Luna view also means subclassing
UIViewComponent.
Create a fresh scene (File > New Scene > Empty).
You need three things in the scene:
Assets/Samples/LunaUI/<version>/Essentials/Prefabs/LunaUIManager.prefab into the scene. This GameObject hosts the LunaUIManager component plus input wiring. Every view subscribes to it for focus management and input handling. See Luna UI Manager for what it does.GameObject > UI > Event System if the scene doesn't have one yet. UI Toolkit input dispatch requires it.GameObject > Create Empty), name it MyView, and add a Panel Renderer component to it (Add Component, search for "Panel Renderer"). PanelRenderer is the component that hosts a UXML tree in a scene — see UI Toolkit Basics for where it sits in the UXML/USS/Panel Settings composition.⚠️ Naming warning. The Project-window create menu calls a
.uxmlasset a "UI Document" (Create > UI Toolkit > UI Document, step 2 below). That's a file in your project — the scene-side host is thePanelRenderercomponent you just added. From here on, this guide says "the PanelRenderer GameObject" for the scene object and "the.uxmlfile" for the asset.
Select the PanelRenderer GameObject and fill its Inspector:
Essentials/Theme/LunaPanelSettings.asset (screen-space) or LunaPanelSettingsWorldSpace.asset (world-space). This single reference is what wires Luna's full theme into your view — same chain you traced in Quick Start step 3. Anything you author from here on inherits the theme.Right-click in the Project view → Create > UI Toolkit > UI Document. Despite the menu wording, this creates a .uxml asset (a file in your project), not a scene GameObject. Name it MyView.uxml. Open it in UI Builder (double-click).
Add a VisualElement as a container, then drop in a Label and a Button from the Library panel. Give them names you can find from code:
name: my-label, set text to "Hello, Luna"name: my-button, set text to "Click me"About what you just added:
<Label>and<Button>are standard Unity UI Toolkit controls — not Luna types. The Luna theme automatically styles every standard UITK control through the USS imported byLunaUIDemoTheme.tss. You'll see Luna's font, button colors, focus ring, hover state, and rounded corners apply without authoring any USS yourself. To use Luna's custom controls (ProgressBar,TabView,RadialLoading,Tooltip, …), drop them in from the Library too — they live under theCupkekGames.Lunanamespace and ship with the package.
⚠️ One outer element. Keep a single top-level
VisualElementwrapping everything (the container you added first).UIViewComponentauto-resolves its view root to the first top-level element of your UXML — with multiple top-level siblings it logs a warning and the others are left out of fades and transitions. One outer container sidesteps that entirely.
Save the file. Back on your PanelRenderer GameObject in the scene, assign MyView.uxml to its Source Asset field — that's what connects the asset to the scene.
If you press Play now, the label and button render with the Luna theme already applied — but they don't do anything yet, and nothing has wired UIViewComponent in.
The minimal version is genuinely small. Create Assets/Scripts/MyView.cs:
using CupkekGames.Luna;
using UnityEngine.UIElements;
public class MyView : UIViewComponent
{
// Fires once, when the panel delivers the visual tree.
protected override void OnUILoaded(VisualElement root)
{
var label = root.Q<Label>("my-label");
var button = root.Q<Button>("my-button");
button.clicked += () => label.text = "Hello, Luna — clicked!";
}
}Subclass UIViewComponent, override OnUILoaded, query your elements — that's the whole contract. The override exists because PanelRenderer builds the visual tree asynchronously: there is no root element to query in Awake(), so the base class hands you the root when it arrives.
Naming gloss —
UIViewComponentvsUIView.UIViewComponentis the MonoBehaviour you attach to the GameObject. Internally it creates and wraps aUIView— the plain C# object that owns the view root, fade, and actions — exposed via itsUIViewproperty (you'll use it in step 5). When these docs say "the view component", they mean the MonoBehaviour.
The minimal version has one gap: the click handler is wired once and never unwired, so it ignores Unity's enable/disable lifecycle. The production-safe version below handles both delivery modes (asynchronous on standalone panels, synchronous during Awake() when the prefab is instantiated under an already-mounted shell) and cleans up after itself:
using CupkekGames.Luna;
using UnityEngine.UIElements;
public class MyView : UIViewComponent
{
private Label _label;
private Button _button;
private int _clicks;
// PanelRenderer delivers the visual tree ASYNCHRONOUSLY — there is
// no root element to query in Awake(). This hook fires exactly once,
// when the tree first arrives. `root` is the view's root element.
protected override void OnUILoaded(VisualElement root)
{
_label = root.Q<Label>("my-label");
_button = root.Q<Button>("my-button");
// Wire the click handler for the first load. Unsubscribe-then-
// subscribe keeps this idempotent (the -= is a safe no-op when
// not subscribed): when the panel is already mounted — e.g. this
// prefab instantiated under a shell — the tree is delivered
// SYNCHRONOUSLY during Awake(), and OnEnable runs after this
// with _button already set.
_button.clicked -= OnClicked;
_button.clicked += OnClicked;
}
private void OnEnable()
{
// Null until the first load — OnUILoaded handles that case above.
// Same -=/+= pattern: on synchronous delivery OnUILoaded already
// subscribed during Awake(), so a bare += here would double-fire.
if (_button == null) return;
_button.clicked -= OnClicked;
_button.clicked += OnClicked;
}
private void OnDisable()
{
if (_button != null) _button.clicked -= OnClicked;
}
private void OnClicked()
{
_clicks++;
_label.text = $"Hello, Luna ({_clicks} clicks)";
}
}Attach this script to the PanelRenderer GameObject from step 1. Since MyView is a UIViewComponent subclass, adding it also adds the UIViewComponent plumbing automatically.
Inspector defaults you can leave as-is:
VisualElement name to scope the view to a sub-tree.0.5 / EaseOutCirc is the Luna default.0 by default.Press Play. The view appears immediately — a standalone view (one not driven by navigation) is born visible; the button increments the label on click; everything wears the Luna theme without a single line of USS in your project.
UIViewComponent exposes Show() and Hide() — they run the fade-in / fade-out animation on the view root:
Hide(); // fade out; the element ends at display: none, GameObject stays active
Show(); // fade back inThere is no "fade out then destroy" helper — for an on-demand view, Hide() it and Show() it again later, or let the navigation stack pop it (navigation destroys multi-instance copies after fade-out for you).
For state-aware logic, subscribe to fade events on the Fade property — a FadeUIElement. Like the element queries, this has to wait for the UI to load (Fade is null before then). WhenUILoaded runs your code immediately if the UI is already loaded, otherwise as soon as it is:
private void Start()
{
WhenUILoaded(() =>
{
Fade.OnFadeIn += OnFadedIn;
Fade.OnFadeOut += OnFadedOut;
});
}
private void OnFadedIn() { /* view fully visible */ }
private void OnFadedOut() { /* view fully hidden */ }Views close on Escape through UI Actions — add a UIViewActionEscape to the view's UIView. The action registers itself while the view is visible and unregisters when it fades out, so stacked views unwind in the right order:
protected override void OnUILoaded(VisualElement root)
{
// ... element queries from step 3 ...
UIView.AddAction(new UIViewActionEscape(Hide));
}(From outside the component you'd wrap it the same way: myView.WhenUILoaded(() => myView.UIView.AddAction(new UIViewActionEscape(myView.Hide)));)
For modal-style views that open on demand and full-screen views that stack (Main Menu → Settings → Credits), use the navigation system — destinations declared in a nav graph get push/pop, backdrops, and Esc-to-pop without hand-wiring. The pre-built Views (Main Menu / Pause / Settings / Save & Load) are all UIViewComponent subclasses you can extend.
For a complete multi-page UI example, see Example: Multi Page UI.
The whole point of this walkthrough: Luna's theme is one Panel Settings reference away from any UXML you author. You didn't write any USS, didn't drag any prefab into your UXML, didn't subclass anything Luna-specific from your UXML side — and yet the result wears the Luna look, animates show/hide, hooks into focus management, and is ready to participate in navigation.
Recommended next: Views. Main Menu, Pause, Settings, Save & Load, Inventory, Credits, Dialogue — most projects extend one of these pre-built screens instead of authoring from scratch, and each one is a UIViewComponent subclass exactly like the view you just wrote.
Also useful from here:
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