First View

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.

1. Set up the scene

Create a fresh scene (File > New Scene > Empty).

You need three things in the scene:

  1. LunaUIManager — drag 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.
  2. EventSystem — add GameObject > UI > Event System if the scene doesn't have one yet. UI Toolkit input dispatch requires it.
  3. Your view GameObject — create an empty GameObject (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 .uxml asset a "UI Document" (Create > UI Toolkit > UI Document, step 2 below). That's a file in your project — the scene-side host is the PanelRenderer component you just added. From here on, this guide says "the PanelRenderer GameObject" for the scene object and "the .uxml file" for the asset.

Select the PanelRenderer GameObject and fill its Inspector:

  • Panel Settings: drag 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.
  • Source Asset: leave empty for now — you'll create a UXML file in step 2.

2. Create the UXML

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:

  • Label → name: my-label, set text to "Hello, Luna"
  • Button → 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 by LunaUIDemoTheme.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 the CupkekGames.Luna namespace and ship with the package.

⚠️ One outer element. Keep a single top-level VisualElement wrapping everything (the container you added first). UIViewComponent auto-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.

3. Write the view script

The minimal version is genuinely small. Create Assets/Scripts/MyView.cs:

csharp
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 — UIViewComponent vs UIView. UIViewComponent is the MonoBehaviour you attach to the GameObject. Internally it creates and wraps a UIView — the plain C# object that owns the view root, fade, and actions — exposed via its UIView property (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:

csharp
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:

  • Panel Renderer — auto-fetches from the same GameObject (or any parent) if left empty.
  • Parent Name — leave empty to use your UXML's outer element as the view root; set to a VisualElement name to scope the view to a sub-tree.
  • Focus Name — element to focus when the view fades in (used for keyboard navigation).
  • Fade Duration / Easing Mode — fade animation. 0.5 / EaseOutCirc is the Luna default.
  • Fade In Delay / Fade Out Delay — optional delay (seconds) before each fade starts. 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.

4. Show, hide, and fade events

UIViewComponent exposes Show() and Hide() — they run the fade-in / fade-out animation on the view root:

csharp
Hide(); // fade out; the element ends at display: none, GameObject stays active Show(); // fade back in

There 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:

csharp
private void Start() { WhenUILoaded(() => { Fade.OnFadeIn += OnFadedIn; Fade.OnFadeOut += OnFadedOut; }); } private void OnFadedIn() { /* view fully visible */ } private void OnFadedOut() { /* view fully hidden */ }

5. Close with Esc

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:

csharp
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.

What you just proved

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:

  • Custom componentsComponents section has per-component pages for Luna's drop-in widgets: ProgressBar, RadialLoading, Tooltip, TabView, PieChart, CooldownWipe, etc.
  • EffectsTransition Animation for entry/exit motion; UI Effects for glow / outline / shadow / gradient via USS.
  • UI ActionsUI Actions — a slot-based action system for view-level behaviors (escape handling, hotkey wiring).

Settings

Theme

Light

Contrast

Material

Dark

Dim

Material Dark

System

Sidebar(Light & Contrast only)

Light
Dark

Font Family

DM Sans

Wix

Inclusive Sans

AR One Sans

Direction

LTR
RTL