.claude/skills/ui-toolkit/SKILL.md
Unity UI Toolkit reference — UXML documents, USS styling, MVVM pattern (ViewModel + Presenter), custom VisualElements, responsive layout, animations, performance guidelines, and complete Figma-to-UI-Toolkit property mapping. Use when building or modifying UI with UI Toolkit, creating UXML/USS files, writing ViewModels or Presenters, designing screens/panels/components, or converting Figma designs to UI Toolkit.
npx skillsauth add punkfuncgames/tetris-clone ui-toolkitInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Framework: Unity UI Toolkit (NOT legacy uGUI/Canvas) Architecture: MVVM (Model → ViewModel → View/Presenter) Bindings: R3 ReactiveProperty → Presenter → VisualElement queries
Assets/UI/
Documents/ # UXML files
Screens/ # Full-screen UIs (HUD, Shop, Inventory)
Panels/ # Reusable panel sections
Components/ # Small reusable elements (buttons, cards, bars)
Popups/ # Modal dialogs, tooltips, confirmations
Dialogs/ # Confirm/cancel modals
Styles/ # USS files
Theme/ # Global theme variables, base styles
variables.uss
base.uss # Optional — shared utility classes only
Screens/
Components/
Shared/ # Utility classes, animations
Resources/ # PanelSettings, ThemeStyleSheet assets
Packages/com.punkfuncgames.ui/Runtime/PunkFuncGames.UI/
ViewModels/ # Observable state holders (R3 ReactiveProperty)
Presenters/ # Binds ViewModel ↔ View (IStartable, IDisposable)
CustomControls/ # C# custom VisualElements ([UxmlElement] partial class)
<ui:UXML xmlns:ui="UnityEngine.UIElements"
xmlns:custom="PunkFuncGames.GameTemplate.Runtime.UI.CustomControls"
editor-extension-mode="False">
<Style src="project://database/Assets/UI/Styles/Theme/variables.uss" />
<Style src="project://database/Assets/UI/Styles/Screens/this-screen.uss" />
<ui:VisualElement name="screen-root" class="screen-root">
<!-- Content here -->
</ui:VisualElement>
</ui:UXML>
name attributes: kebab-case — used for Q<T>("name") queriesblock__element--modifier
.upgrade-panel (block), .upgrade-panel__header (element), .upgrade-panel__button--disabled (modifier)name| Need | Use | NOT |
|------|-----|-----|
| Container/layout | <ui:VisualElement> | <ui:Box> |
| Text display | <ui:Label> | TextElement |
| Button | <ui:Button> | Clickable VisualElement |
| Scroll area | <ui:ScrollView> | Custom scroll |
| Text input | <ui:TextField> | |
| Toggle | <ui:Toggle> | |
| Slider | <ui:Slider> / <ui:SliderInt> | |
| Dropdown | <ui:DropdownField> | |
| Progress bar | <ui:ProgressBar> | |
| List/Repeating | <ui:ListView> | Manual instantiation |
| Foldout | <ui:Foldout> | |
| Tab group | <ui:TabView> (2023.2+) | |
| Custom reusable | <custom:YourControl> | Copying UXML blocks |
<ui:ListView name="item-list" fixed-item-height="60"
selection-type="Single" show-border="true"
class="item-list" />
Always use makeItem/bindItem in C# for ListView binding.
⚠ Do NOT use this block as a template. The actual design system tokens are defined in
Assets/UI/Styles/Theme/variables.uss. Use theread_uxml/extract_theme_tokensMCP tools to read the live values before writing USS. The values below are illustrative examples only.
:root {
/* Colors — read actual values from Assets/UI/Styles/Theme/variables.uss */
--color-primary: <see variables.uss>;
--color-surface-raised: <see variables.uss>;
--color-text-primary: <see variables.uss>;
--color-text-secondary: <see variables.uss>;
--color-border-dim: <see variables.uss>;
/* Typography — project uses large font scale (32px–96px for main UI text) */
--font-size-xs: <see variables.uss>;
--font-size-sm: <see variables.uss>;
--font-size-md: <see variables.uss>;
--font-size-lg: <see variables.uss>;
--font-size-xl: <see variables.uss>;
/* Spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
/* Border */
--border-radius-sm: 4px;
--border-radius-md: 8px;
--border-radius-lg: 16px;
--border-radius-round: 9999px;
}
position: absolute). Elements reflow naturally.position: absolute with percentage coordinates derived from Figma metadata:
left: (child.x - frame.x) / frame.width * 100%.
Always subtract the parent frame's own canvas offset first.transition for all interactive state changesShared/flex-grow/flex-shrink/flex-basis instead of fixed sizes for responsive layout/* Button with states */
.btn-primary {
background-color: var(--color-primary);
color: var(--color-text-primary);
border-radius: var(--border-radius-md);
padding: var(--spacing-sm) var(--spacing-md);
-unity-font-style: bold;
transition: background-color var(--transition-fast), scale var(--transition-fast);
}
.btn-primary:hover { background-color: var(--color-primary-hover); scale: 1.02 1.02; }
.btn-primary:active { background-color: var(--color-primary-pressed); scale: 0.98 0.98; }
.btn-primary:disabled { opacity: 0.5; }
/* Card */
.card {
background-color: var(--color-surface);
border-radius: var(--border-radius-md);
border-width: var(--border-width);
border-color: var(--border-color);
padding: var(--spacing-md);
margin-bottom: var(--spacing-sm);
}
/* Utilities */
.row { flex-direction: row; }
.col { flex-direction: column; }
.center { align-items: center; justify-content: center; }
.grow { flex-grow: 1; }
.shrink-0 { flex-shrink: 0; }
.text-center { -unity-text-align: middle-center; }
.text-secondary { color: var(--color-text-secondary); }
.hidden { display: none; }
gap property — use margins on childrenbox-shadow — fake with nested elements or bordercolor, background-color, border-color, opacity, scale, rotate, translate, width, height, -unity-background-image-tint-color% units are relative to parent, not viewportborder-radius doesn't clip children — set overflow: hidden explicitly<color=#FF0000>, <b>, <size=20> (enable via enable-rich-text="true")Unity Slider and SliderInt have a deep internal hierarchy that fights flex layout. Never use the built-in Unity label — always put the label in a sibling <ui:Label> element.
/* Track background */
.my-slider .unity-base-slider__tracker {
background-color: #222;
border-radius: 9999px;
border-width: 2px;
border-color: #FFF;
height: 37px;
margin-top: -18px; /* center track on dragger */
}
/* Filled portion (left of thumb) */
.my-slider .unity-base-slider__fill {
background-color: var(--color-primary);
border-radius: 9999px;
border-width: 2px;
border-color: #FFF;
}
/* Thumb circle */
.my-slider .unity-base-slider__dragger {
width: 46px;
height: 46px;
border-radius: 9999px;
background-color: #FFF;
border-width: 2px;
border-color: #888;
margin-top: -23px; /* (dragger height / 2) negative */
margin-left: -23px; /* (dragger width / 2) negative */
}
/* Thumb outline ring — hide unless needed */
.my-slider .unity-base-slider__dragger-border { display: none; }
/* Built-in text label — ALWAYS hide when using sibling label */
.my-slider .unity-base-slider__label { display: none; }
/* Built-in value display text field — ALWAYS hide */
.my-slider .unity-base-slider__text-field { display: none; }
/* CRITICAL: min-width fix — prevents slider from overflowing its container */
.my-slider .unity-base-slider__input { min-width: 0; flex-grow: 1; }
.my-slider .unity-base-field__input { min-width: 0; }
Unity Slider has an internal min-width enforcement that causes the track to expand beyond its flex container and visually overlap sibling elements (such as a label placed before the slider in the same row). Symptoms:
min-width only shifts the problem, not fixes itRoot fix (mandatory — apply all three):
flex-shrink: 0 + explicit min-width: Xpx on the sibling label element.my-slider .unity-base-slider__input { min-width: 0; flex-grow: 1; } — remove internal enforcement, allow flex to shrink.my-slider .unity-base-field__input { min-width: 0; } — outer wrapper also enforces min-width<!-- UXML — always use a sibling Label, never the built-in Slider label -->
<ui:VisualElement class="slider-row">
<ui:Label text="Master Volume" class="slider-row__label" />
<ui:Slider name="master-volume-slider" class="settings-slider"
low-value="0" high-value="100" value="70" />
</ui:VisualElement>
/* USS */
.slider-row {
flex-direction: row;
align-items: center;
height: 46px;
margin-bottom: 16px;
}
.slider-row__label {
min-width: 280px;
width: 280px;
flex-shrink: 0; /* MUST: prevents label from shrinking into slider space */
margin-right: 24px;
-unity-text-align: middle-left;
}
.settings-slider {
flex-grow: 1;
max-width: 372px;
min-width: 200px;
height: 37px;
}
/* The mandatory min-width fix */
.settings-slider .unity-base-slider__input { min-width: 0; flex-grow: 1; }
.settings-slider .unity-base-field__input { min-width: 0; }
.settings-slider .unity-base-slider__label { display: none; }
.settings-slider .unity-base-slider__text-field { display: none; }
using UnityEngine.UIElements;
namespace PunkFuncGames.GameTemplate.Runtime.UI.CustomControls
{
[UxmlElement]
public partial class ResourceBar : VisualElement
{
[UxmlAttribute]
public string ResourceName
{
get => _label.text;
set => _label.text = value;
}
[UxmlAttribute]
public float Value
{
get => _progressBar.value;
set => _progressBar.value = value;
}
private readonly Label _label;
private readonly ProgressBar _progressBar;
public ResourceBar()
{
AddToClassList("resource-bar");
_label = new Label("Resource");
_label.AddToClassList("resource-bar__label");
_progressBar = new ProgressBar { highValue = 100f };
_progressBar.AddToClassList("resource-bar__progress");
Add(_label);
Add(_progressBar);
}
}
}
using System;
using R3;
namespace PunkFuncGames.GameTemplate.Runtime.UI.ViewModels
{
public sealed class UpgradeViewModel : IDisposable
{
public ReactiveProperty<string> Name { get; } = new("");
public ReactiveProperty<int> Level { get; } = new(0);
public ReactiveProperty<int> MaxLevel { get; } = new(10);
public ReactiveProperty<long> Cost { get; } = new(0);
public ReactiveProperty<bool> CanAfford { get; } = new(false);
public ReactiveProperty<bool> IsMaxed { get; } = new(false);
public Observable<string> DisplayLevel => Level
.CombineLatest(MaxLevel, (lvl, max) => $"Lv. {lvl}/{max}");
private readonly CompositeDisposable _disposables = new();
public UpgradeViewModel()
{
Name.AddTo(_disposables);
Level.AddTo(_disposables);
MaxLevel.AddTo(_disposables);
Cost.AddTo(_disposables);
CanAfford.AddTo(_disposables);
IsMaxed.AddTo(_disposables);
}
public void Dispose() => _disposables.Dispose();
}
}
using UnityEngine.UIElements;
using VContainer.Unity;
using R3;
namespace PunkFuncGames.GameTemplate.Runtime.UI.Presenters
{
public sealed class UpgradePresenter : IStartable, IDisposable
{
private readonly UIDocument _uiDocument;
private readonly UpgradeViewModel _viewModel;
private readonly CompositeDisposable _disposables = new();
private Label _nameLabel;
private Label _levelLabel;
private Button _upgradeButton;
private EventCallback<ClickEvent> _onUpgradeClick;
public UpgradePresenter(UIDocument uiDocument, UpgradeViewModel viewModel)
{
_uiDocument = uiDocument;
_viewModel = viewModel;
}
public void Start()
{
VisualElement root = _uiDocument.rootVisualElement;
_nameLabel = root.Q<Label>("upgrade-name");
_levelLabel = root.Q<Label>("upgrade-level");
_upgradeButton = root.Q<Button>("upgrade-button");
BindViewModel();
BindInteractions();
}
private void BindViewModel()
{
_viewModel.Name
.Subscribe(v => _nameLabel.text = v)
.AddTo(_disposables);
_viewModel.DisplayLevel
.Subscribe(v => _levelLabel.text = v)
.AddTo(_disposables);
_viewModel.CanAfford
.CombineLatest(_viewModel.IsMaxed, (afford, maxed) => afford && !maxed)
.Subscribe(canUpgrade =>
{
_upgradeButton.SetEnabled(canUpgrade);
_upgradeButton.EnableInClassList("btn-primary--disabled", !canUpgrade);
})
.AddTo(_disposables);
}
private void BindInteractions()
{
_onUpgradeClick = _ => OnUpgradeClicked();
_upgradeButton?.RegisterCallback(_onUpgradeClick);
}
private void OnUpgradeClicked()
{
// Publish command via MessagePipe or call service
}
private void UnregisterCallbacks()
{
_upgradeButton?.UnregisterCallback(_onUpgradeClick);
}
public void Dispose()
{
UnregisterCallbacks();
_disposables.Dispose();
}
}
}
Callback cleanup rule: Always store EventCallback<T> references as fields and call UnregisterCallback in Dispose(). R3 CompositeDisposable only handles reactive subscriptions, not UI Toolkit event callbacks.
builder.RegisterComponent(_mainUIDocument);
builder.Register<UpgradeViewModel>(Lifetime.Scoped);
builder.RegisterEntryPoint<UpgradePresenter>();
One UIDocument per scene. All screens are named elements inside the same UXML. Presenters query by name.
// In LifetimeScope.Configure():
builder.RegisterComponent(_mainMenuDocument); // single UIDocument
builder.Register<MainMenuViewModel>(Lifetime.Scoped);
builder.RegisterEntryPoint<MainMenuPresenter>();
builder.Register<SettingsPopupViewModel>(Lifetime.Scoped);
builder.RegisterEntryPoint<SettingsPopupPresenter>();
// Both Presenters share the same UIDocument — each queries its own named root
Both Presenters receive the same UIDocument instance. They use root.Q<VisualElement>("screen-a-root") and root.Q<VisualElement>("screen-b-root") to operate on isolated subtrees.
// Inspector: two UIDocument components on separate GameObjects
[SerializeField] private UIDocument _hudDocument;
[SerializeField] private UIDocument _dialogDocument;
// In LifetimeScope.Configure():
builder.RegisterComponent(_hudDocument);
builder.Register<HudViewModel>(Lifetime.Scoped);
builder.RegisterEntryPoint<HudPresenter>();
// ⚠ Second UIDocument requires a VContainer InjectionId to disambiguate
builder.RegisterComponent(_dialogDocument)
.WithParameter<UIDocument>(_dialogDocument);
builder.Register<DialogViewModel>(Lifetime.Scoped);
builder.RegisterEntryPoint<DialogPresenter>();
Or use separate child LifetimeScopes, each with their own builder.RegisterComponent(_doc) call — no InjectionId needed since each scope has exactly one UIDocument.
ViewModels have no Unity dependencies — test in EditMode:
[TestFixture]
public sealed class MyViewModelTests
{
private MyViewModel _vm;
[SetUp] public void SetUp() => _vm = new MyViewModel();
[TearDown] public void TearDown() { _vm.Dispose(); _vm = null; }
[Test]
public void IsVisible_DefaultsToFalse() =>
Assert.That(_vm.IsVisible.Value, Is.False);
[Test]
public void NotifyConfirm_EmitsOnConfirmClicked()
{
int received = 0;
_vm.OnConfirmClicked.Subscribe(_ => received++);
_vm.NotifyConfirm();
Assert.That(received, Is.EqualTo(1));
}
[Test]
public void Dispose_DoesNotThrow() =>
Assert.That(() => _vm.Dispose(), Throws.Nothing);
}
[TestFixture]
public sealed class MyPresenterTests
{
private GameObject _go;
private UIDocument _doc;
private PanelSettings _panelSettings;
private MyViewModel _vm;
private MyPresenter _presenter;
private CancellationTokenSource _cts;
[SetUp]
public void SetUp()
{
_cts = new CancellationTokenSource();
_vm = new MyViewModel();
_panelSettings = ScriptableObject.CreateInstance<PanelSettings>();
_go = new GameObject("Test");
_doc = _go.AddComponent<UIDocument>();
_doc.panelSettings = _panelSettings;
// Build element hierarchy matching Presenter's Q<T>() calls
VisualElement root = _doc.rootVisualElement;
root.Add(new Button { name = "confirm-btn" });
root.Add(new Label { name = "message-label" });
_presenter = new MyPresenter(_doc, _vm);
}
[TearDown]
public void TearDown()
{
_presenter?.Dispose();
_vm?.Dispose();
if (_go) Object.DestroyImmediate(_go);
if (_panelSettings) Object.DestroyImmediate(_panelSettings);
_cts?.Cancel();
_cts?.Dispose();
}
[Test]
public async Task DisposeAfterStart_DoesNotThrow()
{
_presenter.Start();
await UniTask.NextFrame(_cts.Token);
Assert.That(() => _presenter.Dispose(), Throws.Nothing);
}
[Test]
public async Task IsVisibleTrue_ShowsRoot()
{
_presenter.Start();
_vm.IsVisible.Value = true;
await UniTask.NextFrame(_cts.Token);
// Assert display style or class list
}
}
Key rules:
UIDocument + PanelSettings on a GameObject)[UnityTest] IEnumerator — use [Test] async Task with UniTask.NextFrame()Assert.AreEqual — use Assert.That(x, Is.EqualTo(y))Dispose presenter and viewmodel in [TearDown]_presenter.Dispose() after Start() must not throw NullReferenceException.panel {
translate: 0 100%;
opacity: 0;
transition: translate var(--transition-normal) ease-out-cubic,
opacity var(--transition-normal);
}
.panel--visible {
translate: 0 0;
opacity: 1;
}
⚠
element.experimental.animationwas removed in Unity 6. Use DOTween instead.
// Fade in
DOTween.To(() => element.style.opacity.value, v => element.style.opacity = v, 1f, 0.25f)
.SetEase(Ease.OutCubic)
.OnComplete(() => { /* callback */ });
// Fade out then hide
DOTween.To(() => element.style.opacity.value, v => element.style.opacity = v, 0f, 0.25f)
.OnComplete(() => element.style.display = DisplayStyle.None);
// Slide in from bottom (translate Y)
element.style.translate = new Translate(0, Length.Percent(100));
DOTween.To(
() => element.resolvedStyle.translate.y.value,
v => element.style.translate = new Translate(0, v),
0f, 0.3f).SetEase(Ease.OutBack);
For sequences, use DOTween.Sequence() with .Append() / .Join() / .AppendCallback().
public static async UniTask FadeIn(VisualElement element, float durationMs = 250f)
{
element.style.display = DisplayStyle.Flex;
element.style.opacity = 0f;
await UniTask.Yield();
element.AddToClassList("fade-in");
await UniTask.Delay((int)durationMs);
}
<ui:VisualElement name="hud-root" class="hud-root">
<ui:VisualElement name="top-bar" class="hud__top-bar">
<custom:ResourceBar name="gold-bar" resource-name="Gold" />
<ui:VisualElement class="grow" />
<ui:Button name="settings-btn" class="btn-icon" />
</ui:VisualElement>
<ui:VisualElement name="game-area" class="hud__game-area" picking-mode="Ignore" />
<ui:VisualElement name="bottom-bar" class="hud__bottom-bar">
<ui:Button name="upgrade-btn" class="btn-action">
<ui:Label text="Upgrade" class="btn-action__label" />
</ui:Button>
</ui:VisualElement>
</ui:VisualElement>
<ui:VisualElement name="upgrade-panel" class="panel upgrade-panel">
<ui:VisualElement class="panel__header">
<ui:Label name="panel-title" text="Upgrades" class="panel__title" />
<ui:Button name="close-btn" class="btn-icon btn-close" />
</ui:VisualElement>
<ui:VisualElement class="tab-bar">
<ui:Button name="tab-attack" text="Attack" class="tab-bar__tab tab-bar__tab--active" />
<ui:Button name="tab-defense" text="Defense" class="tab-bar__tab" />
</ui:VisualElement>
<ui:ScrollView name="upgrade-list-scroll" class="panel__content">
<ui:ListView name="upgrade-list" class="upgrade-list" />
</ui:ScrollView>
<ui:VisualElement class="panel__footer">
<ui:Label name="total-cost" class="text-secondary" />
<ui:Button name="upgrade-all-btn" text="Upgrade All" class="btn-primary" />
</ui:VisualElement>
</ui:VisualElement>
These rules apply to all C# code in ViewModels, Presenters, CustomControls, and tests.
| Pattern | Requirement | Example |
|---------|-------------|---------|
| var keyword | Always use explicit types | VisualElement root = ... NOT var root = ... |
| Chained attributes | One attribute per line | See below |
| == null / != null on UnityEngine.Object | Use implicit bool conversion | if (obj) / if (!obj) NOT if (obj != null) / if (obj == null) |
| Ambiguous Object reference | Disambiguate with alias or fully qualified name | using Object = UnityEngine.Object; or UnityEngine.Object.Destroy(go) |
| Field grouping | Same modifier+type fields grouped together; different groups separated by blank line | See class layout examples |
Never use var. Always declare the explicit type so the code is self-documenting:
// CORRECT
VisualElement root = _uiDocument.rootVisualElement;
Label nameLabel = root.Q<Label>("upgrade-name");
Button upgradeButton = root.Q<Button>("upgrade-button");
int count = 0;
CompositeDisposable disposables = new();
// WRONG — never use var
var root = _uiDocument.rootVisualElement;
var nameLabel = root.Q<Label>("upgrade-name");
var count = 0;
Never chain multiple attributes on the same line:
// CORRECT
[UxmlElement]
[Serializable]
public partial class ResourceBar : VisualElement { }
// WRONG — chained on one line
[UxmlElement, Serializable]
public partial class ResourceBar : VisualElement { }
For any UnityEngine.Object subclass (GameObject, Component, ScriptableObject, etc.), use implicit bool conversion:
// CORRECT
if (_go) Object.DestroyImmediate(_go);
if (!_panelSettings) return;
// WRONG — explicit null comparison on Unity objects
if (_go != null) Object.DestroyImmediate(_go);
if (_panelSettings == null) return;
Note: Pure C# objects (non-Unity) still use standard != null / == null checks.
When a file uses both System.Object and UnityEngine.Object (common in tests with Object.DestroyImmediate), add a using alias at the top:
using Object = UnityEngine.Object;
Or use the fully qualified name inline:
UnityEngine.Object.DestroyImmediate(_go);
ListView for any list > 5 items (virtualized rendering)picking-mode="Ignore" on decorative/non-interactive elementsQ<T>() references in Start/Init — never call in loopsschedule.Execute() for delayed operationsmakeItem/bindItemstyle.* in C#CRITICAL: Always call get_screenshot THEN get_metadata BEFORE writing any code. The generated React code from get_design_context is only a hint — it does NOT capture:
⚠️ Figma MCP coordinate reliability:
get_design_context React/Tailwind output always emits left-0 top-0 for all elements — the generated CSS positions are wrong for absolute layouts.get_metadata to get real x/y coordinates. The metadata XML contains the actual x, y, width, height attributes for every node.get_metadata returns global canvas coordinates. The parent frame itself has an x/y on the canvas (often non-zero). Always subtract the frame's own x/y from each child's x/y to get the correct relative position:
rel_x = child.x - frame.x
rel_y = child.y - frame.y
left% = rel_x / frame.width * 100
top% = rel_y / frame.height * 100
Comparison checklist — verify EACH before writing USS:
get_metadata XML top-to-bottom = back-to-front in Unity DOM(child.x - frame.x) / frame.width * 100% — must subtract frame offsetwidth / frame.width * 100% and height / frame.height * 100%position: absolute)hidden="true" variants (hover states, selected states) — these are art assets to downloadmargin-left modifiersget_design_context output via curlbash tools/svg-clean.sh <folder> on the download folder — this handles all three known Unity SVG incompatibilities in one pass:
filter="url(#...)" attributes (prevents "Invalid image specified" runtime error)<defs><filter>...</filter></defs> blocksvar(--name, #hex) CSS variables with their hardcoded fallback hex<radialGradient> with gradientTransform containing both rotate + scale → ❌ renders as flat last-stop colour → use USS background-color solid fallback or ask artist for PNGvar(--fill-0, ...) CSS variables with non-hex fallbacks (e.g., named colours, none) → ❌ svg-clean.sh only handles #hex fallbacks — fix manuallystroke-only elements (no fill) → ⚠️ may not render or render incorrectly → test, consider using USS border insteadcom.unity.vectorgraphics): This project uses v3.0.0-preview.7.| Unity Version | Package Version | What the Package Provides |
|--------------|----------------|--------------------------|
| Unity 2018.1 – 6.2 | 2.0.0-preview.25 | Full SVG importer (all import types) |
| Unity 6.3+ | 3.0.0-preview.7 | Sprite + uGUI only — VectorImage & Texture2D are built into the engine |
In Unity 6.3+ the built-in Vector Graphics module already handles:
The [email protected] package adds ONLY:
SVGImage component for uGUI (MaskableGraphic subclass)Unlit_Vector, Unlit_VectorGradient, Unlit_VectorUI, Unlit_VectorGradientUI| Feature | Supported? | Notes |
|---------|-----------|-------|
| Paths & shapes | ✅ | Bezier curves, contours, all basic shapes |
| Solid fills | ✅ | Direct color mapping |
| Linear gradients | ✅ | Multi-stop, configurable addressing |
| Radial gradients | ⚠️ | Simple cx/cy/r definitions: ✅. Complex gradientTransform with both rotate and scale: ❌ renders flat last-stop color (e.g., purple→gold gradient → solid gold). Test every radial gradient. |
| Strokes | ✅ | Full stroke rendering |
| Fill rules (NonZero/OddEven) | ✅ | |
| Clipping | ✅ | Shape-based via clipper nodes |
| Texture fills | ✅ | |
| CSS variables | ❌ | var(--fill-0, #COLOR) → always replace with #COLOR |
| Filter effects | ❌ | <filter>, <feGaussianBlur>, <feDropShadow> → export as PNG |
| Text elements | ❌ | SVG 1.1 section 10 — not parsed |
| Masking (per-pixel) | ❌ | SVG 1.1 section 14.4 |
| Interactivity | ❌ | SVG 1.1 section 16 |
| Animations | ❌ | SVG 1.1 section 19 |
svgType in .meta)| svgType | Enum Name | Asset Type | Best For |
|---------|-----------|-----------|----------|
| 0 | VectorSprite | Tessellated sprite (geometry) | SpriteRenderer, resolution-independent |
| 1 | TexturedSprite | Rasterized sprite | SpriteRenderer, fixed resolution |
| 2 | Texture2D | Standard Texture2D | Materials, 3D, anywhere textures used |
| 3 | (Texture2D legacy) | Texture2D rasterization | Fallback when package not installed |
| 4 | UISVGImage | uGUI SVG Image | Canvas-based UI with masking |
For UI Toolkit: Use the built-in "UI Toolkit Vector Image" import type (default in Unity 6.3+). This creates a VectorImage asset — resolution-independent, lightweight, supports 9-slicing.
.meta File ConfigurationWith Vector Graphics package installed (recommended):
svgType: 0
tessellationMode: 0
gradientResolution: 64
keepTextureAspectRatio: 1
This imports as VectorImage — proper gradient support, resolution-independent.
Without package (Texture2D fallback):
svgType: 3
tessellationMode: 1
Forces Texture2D rasterization. Without this, Unity imports SVGs as GameObject and USS reports "Unsupported type GameObject".
After editing .meta files, Unity reimports automatically on focus.
Run bash tools/svg-clean.sh <folder> first — handles items 1, 2, and 3 automatically.
var(--fill-0, #COLOR) → replace with #COLOR (Unity ignores CSS vars entirely — svg-clean.sh handles #hex fallbacks)filter="url(#...)" on any element → "Invalid image specified" spam at runtime. The VectorImage importer produces a null/invalid asset when a filter reference exists. svg-clean.sh strips this attribute AND the <defs><filter> block. If the visual blur/glow matters, re-export that specific layer as PNG from Figma.gradientTransform:
gradientTransform="translate(X Y)" → ✅ renders correctlygradientTransform="translate(X Y) rotate(-N) scale(A B)" → ❌ BROKEN — Unity VectorImage tessellates incorrectly and renders the LAST gradient stop as a flat fill<radialGradient>, check gradientTransform. If it contains both rotate AND scale, treat as broken.background-color as solid approximation, OR ask artist to export the gradient panel as PNG instead. Do NOT try to simplify the gradient transform — the visual result won't match.<stop> in the SVG (e.g., #DEA450 = gold/yellow).gradientResolution controls texture quality (default 64)border alternativestretch-to-fillScale Mode = Scale With Screen Size and Reference Resolution matching the Figma frame size (e.g., 2048×1024)font-size: 48px means 48 screen pixels regardless of display resolution — text will be tiny on 4K and huge on 720p48px means 48 pixels at the reference resolution, and Unity scales proportionally to the actual screen sizeScreen Match Mode = Match Width Or Height with Match = 0.5 for balanced scalingScriptableObject.CreateInstance<PanelSettings>() → set scaleMode, referenceResolution, screenMatchMode, match → AssetDatabase.CreateAsset()Assets/UI/Resources/MainMenuPanelSettings.assetUIDocument component's Panel Settings field in the sceneleft: (x / frameWidth * 100)%width: (elementWidth / frameWidth * 100)%position: absolute with percentage left/top/width/heightwidth — child percentage margin-left resolves against parent width; if parent has no width, margins collapse to 0overflow: hidden on clipping containers — children must use explicit width: 100%; height: 100% not right: 0; bottom: 0 for reliable clippingUI Toolkit has no CSS media queries. Use the Presenter to detect aspect ratio via GeometryChangedEvent and toggle USS classes:
private void BindResponsiveLayout()
{
_onGeometryChanged = _ => UpdateLayoutMode();
// CRITICAL: Register on rootVisualElement, NOT on a child — children may not resolve size in time
_uiDocument.rootVisualElement.RegisterCallback(_onGeometryChanged);
// CRITICAL: Schedule a 100ms retry — rootVisualElement.resolvedStyle is often 0 on Start()
// GeometryChangedEvent fires AFTER layout resolves, but Start() runs BEFORE first layout pass
_root.schedule.Execute(UpdateLayoutMode).ExecuteLater(100);
}
private void UpdateLayoutMode()
{
float width = _uiDocument.rootVisualElement.resolvedStyle.width;
float height = _uiDocument.rootVisualElement.resolvedStyle.height;
// Fallback to Screen.width/height if panel hasn't resolved yet
if (width <= 0 || height <= 0) { width = Screen.width; height = Screen.height; }
if (width <= 0 || height <= 0) return;
bool isLandscape = (width / height) >= 1.4f;
if (isLandscape == _isLandscape && _layoutInitialized) return;
_isLandscape = isLandscape;
_layoutInitialized = true;
// Landscape = DEFAULT (no class needed). Portrait = OVERRIDE class.
if (_isLandscape) _root.RemoveFromClassList("screen--portrait");
else _root.AddToClassList("screen--portrait");
}
CRITICAL layout pattern — landscape is always the default:
/* BASE = landscape (Figma design). No class needed, works from frame 0. */
.main-menu__logo { left: 51%; top: 4%; width: 45%; height: 90%; }
.main-menu__btn { position: absolute; font-size: 48px; }
/* PORTRAIT OVERRIDES — only applied after Presenter detects narrow aspect */
.main-menu--portrait .main-menu__logo { left: 0; top: 3%; width: 100%; height: 46%; }
.main-menu--portrait .main-menu__btn { position: relative; font-size: 32px; min-height: 52px; }
Never gate ALL layout behind a class — if the class isn't applied on frame 0, elements have no size and are invisible.
USS uses descendant selectors per layout mode:
/* Portrait — flex column, centered, larger touch targets */
.main-menu--portrait .main-menu__buttons { flex-direction: column; align-items: center; top: 50%; }
.main-menu--portrait .main-menu__btn { position: relative; font-size: 32px; min-height: 52px; padding: 14px 40px; }
min-height: 52px; padding: 14px 40px)flex-direction: column with centered alignment instead of absolute staircasemargin: 6px 0 between buttons to prevent mis-tapswidth: 100%; -unity-background-scale-mode: scale-to-fit fills full width naturallydisplay: none — it competes with content on narrow screensvariables.usspicking-mode="Ignore" on all decorative elements.uxml → .uss → ViewModel.cs → Presenter.cs → registration snippetbackground-image or -unity-font-definition — never leave orphaned assetsbackground-image + -unity-background-scale-mode: scale-and-cropgradientTransform: ❌ If gradientTransform contains rotate + scale (e.g., Figma's radial gradients with non-trivial orientation), Unity VectorImage renders only the last stop as a flat colour. Fallback: use USS background-color as solid approximation. Inform artist to export as PNG for exact match.<filter>, <feGaussianBlur>, <feDropShadow>): ❌ Unity ignores SVG filters even with Vector Graphics package — export as PNG from Figma insteadgradientTransform first (see SVG Validation Checklist point 3). Simple radial gradients work; complex transform-based ones → PNG.multiply, screen, luminosity, etc.): ❌ unsupported — pre-composite in image editor or use opacity alone as approximation.ttf/.otf, generate SDF asset via Window → TextMeshPro → Font Asset Creator, reference via -unity-font-definition: url("project://database/Assets/UI/Fonts/FontName SDF.asset")-unity-font-definition entirely — Unity falls back to the default font. A broken font URL causes text to render with zero size → elements become invisible..ttf directly from raw.githubusercontent.com/google/fonts/main/ofl/<fontname>/. Do NOT use fonts.google.com/download — it returns HTML even with User-Agent headers.:hover, :active, and :disabled states on interactive elements1906×1024 but the container is only 28% of screen width (≈574×1024), stretch-to-fill will squash the diagonal proportions into a near-rectangle. Instead, use an SVG whose viewBox matches the container's expected aspect ratio (e.g., 574×1024 for a 28% panel). If Figma exports multiple SVGs for the same visual area (one full-width with gradient, one panel-sized with solid fill), prefer the panel-sized one for correct shape and accept the artist must re-export the gradient as PNG separately.VisualElement with overflow: hidden and explicit width/height — UI Toolkit only clips to rectangular bounds, not SVG pathsFlag these issues to the artist for Figma fixes:
viewBox matching the full canvas (e.g., 2048×1024) but is clipped to a 28%-wide panel, the diagonal path proportions are wrong. Request the SVG exported at the panel's own bounding box, or provide a PNG.When a USS change appears to have no effect in the Game view:
element.GetClasses() in the Presenter to verify which USS classes are actually applied at runtime.<Style src="path?fileID=X&guid=Y">. If you moved or renamed a USS file, the GUID changes. Open the UXML in a text editor and verify the src path + GUID still match the .meta file.background-color but the gold/wrong SVG is still rendering, check: (a) another USS class on the same element still has background-image, (b) a parent element has the SVG as background, (c) Unity cached the old VectorImage and needs a full reimport (right-click the SVG asset → Reimport)..parent .child) beats your class selector.display: none from a class you forgot — Check element.resolvedStyle.display in code to verify the element is actually visible.-unity-font-definition URL points to a non-existent SDF asset. Remove the font line entirely to confirm, then regenerate the SDF.background-image has filter="url(#...)" on one of its elements (typically the root <g>). The VectorImage importer produces a null asset and Unity logs this error for every element referencing it. Run bash tools/svg-clean.sh <folder> to strip all filter references. The SVG will render without the drop-shadow/glow effect — if that visual matters, re-export as PNG.SVGImporterEditor:BuildPreviewTexture when you click a .svg asset in the Project window. It is an editor-only warning from the SVG preview renderer, not a runtime error. The SVGs import and render correctly in-game. Ignore it.| Figma | USS Property | USS Value | Notes |
|-------|-------------|-----------|-------|
| Auto layout: Vertical | flex-direction | column | |
| Auto layout: Horizontal | flex-direction | row | |
| No auto layout (absolute) | position | absolute | Avoid — use flex |
| Primary axis: Top/Left | justify-content | flex-start | |
| Primary axis: Center | justify-content | center | |
| Primary axis: Bottom/Right | justify-content | flex-end | |
| Space between | justify-content | space-between | |
| Space around | justify-content | — | ❌ Not supported — fake with margins |
| Counter axis: Top/Left | align-items | flex-start | |
| Counter axis: Center | align-items | center | |
| Counter axis: Bottom/Right | align-items | flex-end | |
| Item spacing N px | child margin-bottom/margin-right | Npx | ❌ No gap — use margins on children |
| Padding T R B L | padding | Tpx Rpx Bpx Lpx | Direct mapping |
| Resizing: Fixed W × H | width / height | Wpx / Hpx | Direct |
| Resizing: Hug contents | — | (default) | Don't set width/height |
| Resizing: Fill container | flex-grow | 1 | Also flex-shrink: 1; flex-basis: 0 |
| Wrap | flex-wrap | wrap | |
| Clip content | overflow | hidden | |
| Figma | USS Property | Notes |
|-------|-------------|-------|
| Solid fill RGBA | background-color | rgba(R,G,B,A) or #RRGGBB. Figma 0-1 → multiply by 255 |
| Multiple fills | — | ❌ One background-color only. Use nested elements |
| Linear gradient | background-image | ✅ With Vector Graphics package — import SVG with gradient as VectorImage. Without package → export as PNG texture |
| Radial gradient | background-image | ⚠️ With Vector Graphics package — simple cx/cy/r radial gradients ✅. Complex gradientTransform (rotate+scale) ❌ renders as flat last-stop colour → use background-color or PNG export |
| Image fill | background-image | url("path") or resource("name") |
| Image fill: Scale to fill | -unity-background-scale-mode | scale-and-crop |
| Image fill: Scale to fit | -unity-background-scale-mode | scale-to-fit |
| Fill opacity N% | opacity or rgba alpha | Use rgba for fill-only transparency |
| Color styles | USS variable | var(--color-name) |
| Background tint | -unity-background-image-tint-color | Multiplies with image |
| Figma | USS Property | Notes |
|-------|-------------|-------|
| Stroke: Inside | border-width + border-color | USS borders are always inside |
| Stroke: Outside | — | ❌ Workaround: wrapper with padding + bg |
| Stroke: Center | — | ❌ Use inside (closest match) |
| Per-side stroke | border-top-width / -color etc. | Individual sides supported |
| Corner radius uniform | border-radius | Direct mapping |
| Corner radius per-corner | border-top-left-radius etc. | Individual corners |
| Corner smoothing (squircle) | — | ❌ Standard circular arcs only |
| Figma | USS Property | Notes |
|-------|-------------|-------|
| Font family | -unity-font-definition | url("Font SDF.asset") — use TMP SDF |
| Font size | font-size | Direct Npx |
| Font weight | -unity-font-style | ❌ Only normal, bold, italic, bold-and-italic. Use separate font assets for other weights |
| Line height | — | ❌ Use C# style.lineHeight (2023.2+) or padding |
| Letter spacing | letter-spacing | Direct Npx |
| Text align | -unity-text-align | Combined H+V: upper-left, middle-center, lower-right etc. |
| Text decoration | -unity-text-decoration | underline, strikethrough (limited) |
| UPPERCASE | — | ❌ No text-transform. Apply in C# .ToUpper() |
| Ellipsis truncation | text-overflow | ellipsis + overflow: hidden + explicit width |
| Text color | color | #RRGGBB |
| Rich text | UXML enable-rich-text="true" | <b>, <i>, <color=#FFF>, <size=20> |
| Figma | USS Property | Notes |
|-------|-------------|-------|
| Opacity | opacity | 0.0 — 1.0. Affects element + all children |
| Drop shadow | — | ❌ Use 9-slice shadow sprite behind element |
| Inner shadow | — | ❌ Use border or inner overlay |
| Layer/Background blur | — | ❌ Custom shader or render texture |
| Blend mode | — | ❌ Custom rendering |
| Rotation | rotate | Ndeg — visual only, no layout effect |
| Scale | scale | N N (X Y). Visual only |
| Translate | translate | Xpx Ypx — visual offset |
| Visibility: Hidden | visibility | hidden — preserves layout space |
| Visibility: Removed | display | none — removes from layout |
| 9-slice | -unity-slice-left/right/top/bottom | + -unity-slice-scale: 1px |
| Figma State | USS Pseudo-class | Notes |
|-------------|-----------------|-------|
| Default | (base selector) | |
| Hover | :hover | |
| Pressed | :active | |
| Focused | :focus | Keyboard/gamepad |
| Disabled | :disabled | SetEnabled(false) in C# |
| Selected | .--selected (custom) | Toggle class in C# |
| Checked | :checked | For Toggle elements |
| Figma | UI Toolkit | Implementation |
|-------|-----------|----------------|
| Component | [UxmlElement] C# class | Extends VisualElement |
| Component variant | USS modifier class | .component--variant |
| Component property: Bool | [UxmlAttribute] bool | Toggle USS class |
| Component property: Text | [UxmlAttribute] string | Bind to Label.text |
| Instance | <custom:YourComponent> | UXML tag |
| Instance override | UXML attribute | <custom:Comp label="Override" /> |
| Frame + Auto Layout | <ui:VisualElement> + flex | Standard container |
| Group | — | Flatten — skip unless has layout purpose |
| Boolean operation | Export as SVG/PNG | Rasterize |
| Mask | overflow: hidden | On parent |
| Figma | UI Toolkit |
|-------|-----------|
| Frame + Auto Layout | <ui:VisualElement> + flex |
| Text | <ui:Label> |
| Rectangle | <ui:VisualElement> + background |
| Image/Shape fill | background-image + scale-mode |
| Component | [UxmlElement] C# class |
| Instance | <custom:Component> UXML tag |
| Variant | USS modifier --variant |
| Auto Layout Vertical | flex-direction: column |
| Auto Layout Horizontal | flex-direction: row |
| Fill | flex-grow: 1 |
| Hug | (default, no width/height) |
| Fixed | explicit width + height |
| Item spacing | margin on children |
| Scroll | <ui:ScrollView> |
| Repeating list | <ui:ListView> |
| Pointer passthrough | picking-mode="Ignore" |
Figma API: { r: 0.09, g: 0.09, b: 0.18, a: 1.0 }
→ multiply by 255 and round
USS hex: #17172E
Figma API: { r: 0.29, g: 0.56, b: 0.85, a: 0.8 }
→ rgba for alpha < 1
USS rgba: rgba(74, 143, 217, 0.8)
Figma: opacity 60% → USS: opacity: 0.6
| Feature | Workaround |
|---------|-----------|
| gap | margin-bottom/margin-right on children |
| Drop shadow | 9-slice shadow sprite behind element |
| Inner shadow | Semi-transparent border + inner overlay |
| Gradient fill (linear / simple radial) | ✅ With Vector Graphics package: import SVG gradient as VectorImage. Without: gradient texture as background-image |
| Gradient fill (radial with rotate+scale transform) | ❌ VectorImage renders last-stop colour. Use background-color solid fallback or request PNG from artist |
| Background blur | Custom shader on RenderTexture overlay |
| Blend modes | Custom shader |
| Font weights 100-900 | Separate font assets per weight |
| Line height | C# style.lineHeight (2023.2+) or padding |
| Aspect ratio | C# GeometryChangedEvent callback |
| CSS Grid | Nested flex containers |
| z-index | DOM order controls stacking — reorder UXML |
| :last-child | Apply/remove class via C# |
| :nth-child | Apply classes via C# in bindItem |
| calc() | Compute in C# and set inline style |
| CSS @keyframes | C# experimental.animation or DOTween |
| cursor: pointer | C# style.cursor with texture |
| Scroll snap | Custom C# scroll controller |
Naming: kebab-case layers, prefix interactive (btn-, input-, toggle-, slider-), prefix containers (panel-, section-, row-)
Structure: Auto Layout on every frame, proper resizing (Fill/Hug/Fixed), no Groups for layout, flatten boolean ops, max 6 levels deep
Styles: Color Styles for all colors → USS variables, Text Styles → USS classes, document spacing scale (4/8/12/16/24/32px)
Export: Icons as SVG/PNG @2x, 9-slice guides on stretchable backgrounds, consistent icon sizes (16/20/24/32/48px)
development
WalletModule reference — currency management with BigDouble support, reactive properties, caps, lifetime stats, and persistence. Use when working with currencies, wallets, or financial systems.
development
UnlockConditionModule reference — composable unlock conditions using ScriptableObjects with AND/OR/NOT logic, stat/currency/upgrade/prestige/gamestate/boolean checks, reactive service layer with progress tracking. Use when implementing unlock systems, gating, or progression requirements.
development
UndoModule reference — command pattern with undo/redo stacks, command merging, and reactive state. Use when implementing undo/redo, undoable actions, or command patterns.
content-media
UIModule reference — panel management, dialog service, notifications, loading screen, and localized UI components. Use when working with UI panels, popups, dialogs, or localized text/images.