This page is for projects whose UI lives in uGUI: Canvases, RectTransforms, TextMeshPro. If your project already uses UI Toolkit, skip straight to Adopt Luna in an Existing UITK Project.
Moving from uGUI to Luna is two migrations, and it pays to keep them separate in your head: first uGUI to UI Toolkit (a different UI system, not a different version of the same one), then plain UITK to Luna (a design system + workflow on top of UITK). The second half is the staged path on the UITK adoption page; this page gets you through the first half and tells you what not to port.
Both UI systems run side by side in one project without conflict, so you migrate screen by screen while the game stays shippable.
Four project-level things decide how the whole migration feels. Settle them first; each one is cheap now and expensive to retrofit after ten screens.
Input System. UITK's runtime input needs the Input System package with a project-wide actions asset assigned. Check Project Settings ▸ Player ▸ Active Input Handling — if your project is on the old Input Manager, that switch is the real first task, and it touches gameplay code, not just UI. Luna ships a configured actions asset you can point at to get moving; swap in a project-specific one later. Some Luna views also expect a PlayerInput component alongside the actions asset, not just the asset. See Input Device Manager.
Fonts. A TMP_FontAsset is not a UITK font asset. UITK uses TextCore FontAsset, a different class, so you cannot reuse the ones TextMeshPro already built — regenerate from the source .ttf and wire the fallback chain into the theme (Theme Stack). Two settings to get right at creation time rather than later:
Your uGUI plugin inventory. List what your screens actually depend on before you port one, because each has a different landing place and a couple have none:
| uGUI plugin | Lands on |
|---|---|
| Shadow / glow / outline plugins (TrueShadow, UIEffect, …) | UI Effects, or UI Effect Baking when you want zero runtime cost |
| Particle-over-UI plugins (UIParticle, …) | UI Render |
| Fly-to-target / collect animations | UI Attractor |
| Pooled/looping scroll lists | ui:ListView virtualization (ListView, List & ScrollView) |
| Tweening on RectTransforms | USS transitions + Transition Animation / Interaction Juice |
| Soft masks / rounded clipping | overflow: hidden + border-radius (Masking) |
| Gradient fills | No USS equivalent — bake a texture (see What has no equivalent) |
Panel Settings. Scaling strategy lives on the Panel Settings asset, not on each screen (Theme Stack). Decide now whether you author in device pixels or density-independent pixels, because it sets what every number in every USS file means. Note that Luna drives panel scale itself through the Responsive system — Panel Settings used by Luna views run in ConstantPixelSize and the responsive config is the authority.
Do UI Toolkit Basics first; it is short. Then use this table as your translation dictionary while porting:
| uGUI habit | UITK / Luna replacement |
|---|---|
| Canvas + CanvasScaler | Panel Settings asset referenced by a PanelRenderer. Luna ships a configured one (LunaPanelSettings); scaling strategy lives there, not per screen |
| One GameObject per element, RectTransform anchors | A UXML document; elements are lightweight VisualElements laid out by flexbox, not GameObjects |
| Nested prefabs for reusable widgets | UXML templates + USS classes (taught in the next section); Luna's components replace the widget prefabs you built yourself |
| Image components + sprite slicing for panels, borders, glow | USS backgrounds and Luna's UI Effects (glow, outline, shadow, gradient, pattern) with no textures at all |
| TextMeshPro | UITK's built-in SDF text. Luna's theme wires the font fallback chain; Text Effects covers the animated-text ground TMP plugins used to |
Button.onClick wired in the Inspector | Query the element once, subscribe in code; under Luna views this lives in OnUILoaded (First View) |
| Horizontal/VerticalLayoutGroup | Flexbox: flex-direction + justify-content; group spacing becomes a margin on each child (Layout) |
| ContentSizeFitter | Nothing — flex sizes to content by default; you opt out with an explicit size |
| LayoutElement min/preferred/flexible | min-* / max-* / flex-grow |
| ScrollRect (+ pooled list plugin) | ui:ScrollView for short heterogeneous content, ui:ListView for long homogeneous lists (ListView) |
| Mask / RectMask2D | overflow: hidden, plus border-radius for rounded clipping (Masking) |
| Sibling index for draw order | Document order — there is no z-index. Reordering the paint means moving the element |
Screen.safeArea handling per screen | luna-safe-area wrappers + the Responsive config |
| Animator / tweening plugins on RectTransforms | USS transitions plus Luna's Transition Animation presets and Interaction Juice |
CanvasGroup alpha + SetActive for show/hide | Luna view lifecycle: stack push/pop with automatic fades and occlusion (Navigation Graph) |
| Localization component per label | A runtime binding, so language changes re-resolve live (Localization) |
| World-space Canvas | World-space UITK via LunaPanelSettingsWorldSpace (Theme Stack) |
| Particles / 3D models over UI via camera stacking or plugins | UI Render: camera to render texture to UI element, managed for you |
The table row deserves its own mechanic, because it is the one uGUI habit you keep: a reusable widget is its own .uxml file, and other documents stamp instances of it. A gold resource pill that appears on five screens is one file:
<!-- Components/GoldPill.uxml : the file's root element IS the component -->
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:VisualElement class="gold-pill">
<ui:VisualElement class="gold-pill__icon" />
<ui:Label name="amount" class="gold-pill__amount" text="0" />
</ui:VisualElement>
</ui:UXML>Consumers declare the import once, then instance it like a prefab:
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:Template name="GoldPill" src="../Components/GoldPill.uxml" />
<ui:VisualElement class="topbar">
<ui:Instance template="GoldPill" name="gold" />
<ui:Instance template="GoldPill" name="gems" class="topbar__pill" />
</ui:VisualElement>
</ui:UXML>What carries over from prefab thinking, and what does not:
<ui:Template> declaration for you when you drag one .uxml into another; src is a relative path.TemplateContainer, one extra element between the parent and your component's root. Placement styling (size, margins, flex) belongs on the instance (class on <ui:Instance> lands on that container); the component's own look stays inside the template file.root.Q("gold").Q<Label>("amount") finds the amount label of that pill specifically. Give each instance a unique name; keep inner names generic.gold-pill--compact) or attributes your controller reads; live data (the amount) is written from C#, the same place your uGUI controller set TMP_Text.text.VisualTreeAsset: serialize a reference and call Instantiate() to spawn instances at runtime (this is exactly what ListView.makeItem does for rows).The Showcase shell is built this way: TopBanner and TabBar are templates instanced by the hub screen, and the Responsive safe-area example shows them in place.
There are two honest strategies, and picking the wrong one is the most expensive mistake on this page. They differ by roughly a factor of five in effort.
Rebuild the screen's structure, then let the theme and components supply the visual layer you used to hand-place. Do this whenever you are allowed to change how the screen looks — new screens, unshipped screens, anything mid-redesign.
class="btn btn-solid" buttons, a ProgressBar for the resource bar).Screens that are mostly decoration (sliced sprites, baked glows, drop shadows) usually come out smaller in UITK because UI Effects generates that look from the shader.
A live game with an art-driven identity does not get to redesign its Home screen as a side effect of a tech migration. If players would notice the difference, you owe the screen pixel parity, and then the rule above inverts: pixels are the spec.
The mistake here is eyeballing it. Screenshots lie about a few px, prefab inspectors lie about runtime values, and "close enough" compounds across twenty elements. Extract instead:
left/top. See the warning below.Budget accordingly: parity porting is several times slower than a redesign port, and the difference is almost entirely measurement, not authoring.
Extracted px are not a layout. Under a match-height scaler your reference width changes with aspect ratio, so a design captured at one width silently breaks at another — fixed-width grids wrap, centred rows hug the left edge. Convert absolute x offsets into flex intent as you go: centring is
align-items: centerorleft: 50%; translate: -50% 0; right-alignment is aflex-grow: 1spacer; a card grid isflex-wrapwith percentage cell widths, and the design size becomes amax-widthon the grid, not the cell.aspect-ratio(Unity 6000.5+) keeps a component's proportions while its width is driven by the parent.
The page promised this, so here it is. Leave these behind:
anchorMin/anchorMax/pivot/sizeDelta combination has a shorter flexbox expression. Porting anchors literally produces a screen that is rigid in exactly the places flexbox would have been free.VerticalLayoutGroup + ContentSizeFitter + LayoutElement sandwich usually collapses to two USS properties.SetActive chains, CanvasGroup alpha tweens and manual back-button stacks are replaced wholesale by the view lifecycle and Navigation Graph.Generic UITK/Unity behaviour that surprises people arriving from uGUI. None of these are Luna-specific.
Text will not match TMP on the first try. Four separate causes, in the order they bite:
atlasPadding / samplingPointSize). If your TMP asset was baked at a generous ratio and the UITK one at a tight one, large text loses its outline into detached blobs. Match the ratio._FaceDilate has no USS property. -unity-font-style: bold approximates it, since TextCore's fake bold dilates the SDF face.text-shadow. Keep the shadow's blur at least as large as any horizontal offset, or diagonal glyph edges show a notch.letter-spacing — and note UITK applies letter-spacing after the last character too, which drifts centred text; add matching left padding to re-centre.Auto-sizing text. In Unity 6000.5 the property is -unity-text-auto-size: best-fit <min> <max>. Older property names are silently ignored — no import warning — so always confirm the text actually shrinks. Unlike TMP's auto-size, best-fit also accounts for line height and effects, so a box sized to the old TMP rect renders visibly smaller; give it roughly 20% more room. See Auto Size Text.
There is no text-transform. For text bound through Luna, pass casing: LunaTextCasing.Upper to BindLocalizedText: it re-cases with the active locale's culture (Turkish dotted/dotless i safe) on every locale change, and it works on Button too. For text outside the binding, uppercase in code with the culture-aware ToUpper, not ToUpperInvariant.
Selector specificity, twice. A bare Label type selector loses to the theme's .unity-label class rule, so a "global" text reset that targets the type does nothing. And a theme rule like .btn-solid > * beats a type selector too, which is why a label nested in a default-look button quietly takes the button's font size unless it carries its own class. Also worth knowing: default Label margin and padding are non-zero and asymmetric, which shifts every measured rect by a few px until you reset them.
Theme sheets do not resolve var() in their own rule declarations. Custom properties defined in a theme and consumed by a component sheet work fine, but a var() used inside a rule that reaches the panel through a .tss @import silently drops the whole declaration. Use literals at theme level. Relatedly: editing a .uss that a .tss imports does not re-flatten the theme — force-reimport the .tss or your play-mode session keeps serving the old rules.
A class that sets -unity-slice-* will mangle any sprite you draw on that element. In Luna the slice settings live on btn-solid (the default button look), so a sprite-skinned button should carry bare btn: it keeps the press/hover feel with no slices, no size clamp, and no font override. If a sprite renders as garbage and the console mentions borders "overridden by style slices", the element is carrying btn-solid (or another slicing class) underneath its art: drop the class or move the art to a child.
Absolute children ignore safe-area padding. Safe area is applied as padding on a wrapper, so absolutely-positioned children are unaffected. Screens built as "one absolute container with everything inside" do not respond to notches at all; make the safe-area wrapper a flex column and let its children be relative.
Scroll constants are in panel pixels. mouse-wheel-scroll-size defaults to 18, which is a crawl in a large reference space — rescale every px-based scroll constant for your panel. Physics knobs (elasticity, scroll-deceleration-rate, touch-scroll-type) are C# properties and UXML attributes, never USS. uGUI's ScrollRect and UITK's ScrollView use the same velocity decay formula, so an existing deceleration rate transfers 1:1. A ListView builds its own ScrollView, so configure that one in code. Finally, momentum and rubber-band are touch-only — the mouse wheel path has neither, so judge scroll feel in the Simulator or on device, never with a trackpad.
Virtualized lists must reset state, not just set it. In bindItem, use EnableInClassList for every state class on the row. Setting only the new state leaves a recycled row wearing the previous item's medal, badge or colour.
Q(name) can hit a hidden duplicate. Once several views coexist in one panel, a name can resolve to a 0×0 element in an inactive tree. When it matters, query all matches and take the one with a non-zero worldBound.
A standalone scene inherits nothing your boot flow configures. The classic symptom is a demo scene that feels smooth in the editor and sluggish on device, because nothing set Application.targetFrameRate and iOS defaulted to 30 fps — and the editor ignores targetFrameRate, which is exactly why it never shows there. Anything your boot sequence sets, a standalone UI scene needs its own copy of.
The Device Simulator silently resizes the UITK panel and changes Screen.safeArea. That is useful when testing insets and ruinous during a parity pass, where it makes your UITK captures a different scale from a uGUI reference taken in the same session. Close it before measuring. For the same reason, keep exactly one Game View open: the play loop and screen-capture calls can end up following different views.
Never put a margin on an element with aspect-ratio. Flex solves height first and the element comes out the wrong size. Put gutters on a wrapping cell instead.
Small, honest list — plan around these rather than discovering them mid-port:
experimental.animation helper.z-index. Paint order is document order.Both systems coexist, so the practical shape of the migration is: theme first, then one screen at a time, newest screens first.
sortingOrder versus the Panel Settings sort order — set them in explicit bands rather than discovering the overlap during a demo.From here your migrated screens are ordinary UITK, and the rest is exactly the UITK adoption path:
PanelRenderer. Since you are rebuilding the screen anyway, going straight to the end state costs nothing extra, unlike the UITK-native case where screens already work.And check Views before porting each screen; Settings, Save & Load, Main Menu, Pause, and Inventory already exist as extendable views. If something misbehaves in a way this page did not cover, Troubleshoot is the next stop.
uGUI-to-UXML conversion is mechanical enough that AI agents handle it well when they can read the target patterns. This site publishes every docs page as raw markdown for exactly that; see AI Development for setup and migration prompts.
Two things make agent-driven porting work much better in practice: give the agent a way to extract ground truth itself (an editor script that dumps the live hierarchy beats screenshots), and have it write down each rule it discovers as it ports. The trap list above is what that habit produces after a few screens.
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