Introduction
Unity’s UI Toolkit provides a modern way to create user interfaces using UXML, USS, and C#.
Instead of creating every UI element directly in C#, you can define your interface using UXML and then access those elements from your scripts at runtime.
A typical UI Toolkit workflow looks like this:
UXML
↓
UIDocument
↓
rootVisualElement
↓
Q() / Query()
↓
C# Script
↓
Events & UI Logic
In this guide, we’ll look at several practical ways to access UI elements from C#.
1. Accessing UI Elements Using UIDocument and UXML
The most common approach is to use the UIDocument component.
A UIDocument loads a UXML hierarchy and exposes its root through:
rootVisualElement
From there, you can search for individual elements.
Step 1: Create Your UXML
Create a UXML document containing elements such as:
- Button
- Label
- TextField
- Image
- VisualElement
For example:
<ui:UXML
xmlns:ui="UnityEngine.UIElements">
<ui:Label
name="myLabel"
text="Welcome!" />
<ui:Button
name="myButton"
text="Click Me" />
</ui:UXML>
The important part is the name attribute:
name="myButton"
This allows the element to be found from C#.
2. Accessing UXML Elements From C#
Attach a UIDocument component to a GameObject and assign your UXML document.
Then use:
using UnityEngine;
using UnityEngine.UIElements;
public class UIToolkitExample : MonoBehaviour
{
private UIDocument uiDocument;
private void OnEnable()
{
uiDocument = GetComponent<UIDocument>();
VisualElement root = uiDocument.rootVisualElement;
Button button = root.Q<Button>("myButton");
Label label = root.Q<Label>("myLabel");
button.clicked += () =>
{
Debug.Log("Button clicked!");
};
label.text = "Hello, UI Toolkit!";
}
}
Now your C# script can communicate directly with the elements defined in UXML.
3. Understanding rootVisualElement
The rootVisualElement is the entry point to your UI hierarchy.
Think of it as the parent container for the UI loaded by the UIDocument.
For example:
UIDocument
│
└── rootVisualElement
│
├── Label
├── Button
├── TextField
└── Image
You can start your queries from:
var root = uiDocument.rootVisualElement;
Then search inside the hierarchy.
4. Using Q<T>() to Find an Element
The Q<T>() method is one of the most convenient ways to retrieve a specific UI element.
For example:
Button button = root.Q<Button>("myButton");
Here:
Buttonspecifies the expected element type."myButton"is the element’s name.
Similarly:
Label label = root.Q<Label>("myLabel");
TextField textField = root.Q<TextField>("myTextField");
Image image = root.Q<Image>("myImage");
This is particularly useful when you know exactly which element you want.
5. Querying Without a Name
You can also search for the first element of a specific type.
For example:
Image image = root.Q<Image>();
This searches the hierarchy for an Image.
However, if your UI contains several images, it’s better to give them meaningful names:
Image playerIcon = root.Q<Image>("playerIcon");
This makes your code easier to understand and maintain.
6. Accessing Multiple Elements With Query()
Sometimes you don’t want one element.
You want multiple elements of the same type.
That’s where Query<T>() is useful.
For example:
var buttons = root.Query<Button>().ToList();
You can then loop through them:
foreach (var button in buttons)
{
button.clicked += () =>
{
Debug.Log("A button was clicked!");
};
}
You may need:
using System.Linq;
for ToList().
7. Querying by USS Class
UI Toolkit also allows you to organize elements using USS classes.
For example:
<ui:Button
name="playButton"
class="menuButton"
text="Play" />
Another button could use the same class:
<ui:Button
name="settingsButton"
class="menuButton"
text="Settings" />
Now both buttons belong to:
menuButton
You can query them together:
var buttons = root.Query<Button>(className: "menuButton").ToList();
Then:
foreach (var button in buttons)
{
button.clicked += () =>
{
Debug.Log("Menu button clicked!");
};
}
This is useful when several UI elements should share behavior.
8. Using USS for Styling
UXML defines the structure.
USS defines the appearance.
For example:
.menuButton {
width: 200px;
height: 60px;
}
Your architecture becomes:
UXML
↓
Structure
USS
↓
Appearance
C#
↓
Behavior
This separation is one of the biggest advantages of UI Toolkit.
9. Finding Elements by Name
Names are especially useful for important UI elements.
For example:
<ui:Label name="scoreLabel" />
Then:
Label scoreLabel = root.Q<Label>("scoreLabel");
You can update it during gameplay:
scoreLabel.text = "Score: " + score;
For a game, you might have:
Game UI
│
├── scoreLabel
├── levelLabel
├── timerLabel
├── pauseButton
├── restartButton
└── gameOverPanel
Then retrieve them in your script:
scoreLabel = root.Q<Label>("scoreLabel");
levelLabel = root.Q<Label>("levelLabel");
timerLabel = root.Q<Label>("timerLabel");
pauseButton = root.Q<Button>("pauseButton");
restartButton = root.Q<Button>("restartButton");
gameOverPanel = root.Q<VisualElement>("gameOverPanel");
10. Registering Button Events
Once you’ve retrieved a button, you can register an event.
Button playButton = root.Q<Button>("playButton");
playButton.clicked += StartGame;
Then create the method:
private void StartGame()
{
Debug.Log("Game Started!");
}
This is cleaner than putting large amounts of UI logic directly inside the initialization method.
11. Removing Button Events
If a UI object can be initialized multiple times, it’s important to consider event subscriptions.
For example:
button.clicked += StartGame;
If you repeatedly register the same callback without removing it, the callback can potentially execute multiple times.
You can remove it with:
button.clicked -= StartGame;
A common pattern is:
private void OnEnable()
{
button.clicked += StartGame;
}
private void OnDisable()
{
button.clicked -= StartGame;
}
This is particularly useful for UI that is repeatedly enabled and disabled.
12. Checking for Missing Elements
During development, it’s useful to make sure your UI elements actually exist.
For example:
Button button = root.Q<Button>("myButton");
if (button == null)
{
Debug.LogError("myButton was not found!");
return;
}
This can save time when debugging UXML naming mistakes.
13. Recommended Naming Convention
For larger projects, consistent naming helps enormously.
For example:
btnPlay
btnSettings
btnQuit
lblScore
lblLevel
lblTimer
imgPlayer
imgIcon
panelPause
panelGameOver
panelSettings
txtPlayerName
Then your C# code becomes easy to read:
Button btnPlay;
Label lblScore;
VisualElement panelGameOver;
You don’t have to use this exact convention. The important thing is to choose one and use it consistently.
14. A Complete Example
Here’s a simple game UI example:
using UnityEngine;
using UnityEngine.UIElements;
public class GameUI : MonoBehaviour
{
private UIDocument uiDocument;
private Label scoreLabel;
private Label levelLabel;
private Button playButton;
private Button pauseButton;
private VisualElement gameOverPanel;
private void OnEnable()
{
uiDocument = GetComponent<UIDocument>();
VisualElement root = uiDocument.rootVisualElement;
scoreLabel = root.Q<Label>("scoreLabel");
levelLabel = root.Q<Label>("levelLabel");
playButton = root.Q<Button>("playButton");
pauseButton = root.Q<Button>("pauseButton");
gameOverPanel = root.Q<VisualElement>("gameOverPanel");
playButton.clicked += OnPlayClicked;
pauseButton.clicked += OnPauseClicked;
scoreLabel.text = "Score: 0";
levelLabel.text = "Level 1";
gameOverPanel.style.display = DisplayStyle.None;
}
private void OnDisable()
{
playButton.clicked -= OnPlayClicked;
pauseButton.clicked -= OnPauseClicked;
}
private void OnPlayClicked()
{
Debug.Log("Play clicked");
}
private void OnPauseClicked()
{
Debug.Log("Pause clicked");
}
public void UpdateScore(int score)
{
scoreLabel.text = $"Score: {score}";
}
}
This provides a clean separation:
UXML
↓
Defines UI
C#
↓
Finds UI
C#
↓
Controls behavior
USS
↓
Controls appearance
15. A Common Mistake: Loading UXML Twice
If you’re using a UIDocument component with a UXML assigned to its Source Asset, you generally don’t need to manually call:
uxmlAsset.CloneTree();
root.Add(uxml);
The UIDocument already loads its assigned UXML hierarchy.
In that situation, simply use:
var root = GetComponent<UIDocument>().rootVisualElement;
and query the elements:
var button = root.Q<Button>("myButton");
Manual CloneTree() is useful when you intentionally want to instantiate an additional VisualTreeAsset into another container.
This distinction is important because loading the same UXML twice can result in duplicate UI elements.
Best Practices
When working with Unity UI Toolkit, keep these principles in mind:
Use UXML for structure
What elements exist?
Use USS for styling
How should they look?
Use C# for behavior
What should they do?
Give important elements meaningful names
root.Q<Button>("playButton");
Use Query() when dealing with collections
root.Query<Button>();
Avoid repeatedly cloning the same UXML
Let UIDocument manage its assigned Source Asset unless you intentionally need additional instances.
Conclusion
Unity UI Toolkit provides a powerful system for building modern game interfaces.
The basic workflow is straightforward:
Create UXML
↓
Attach UIDocument
↓
Get rootVisualElement
↓
Q<T>() / Query<T>()
↓
Register Events
↓
Control UI From C#
Once you understand UIDocument, rootVisualElement, Q(), Query(), UXML, and USS, you can build everything from simple menus to complete game interfaces using a clean separation between structure, styling, and behavior.
For Unity developers, learning UI Toolkit is especially valuable for building scalable interfaces that can be reused across multiple games and projects.