UnityEngine.UIElements.ListView is UI Toolkit's virtualized list control. It renders only the rows that fit in the viewport and recycles them as you scroll, which makes it the right choice for any list whose length you don't control — save slots, quest logs, inventories, leaderboards.
That recycling is also why it trips people up: a ListView is not a container you put children into. It is a contract between your data and a small pool of reusable row elements, and you have to supply both sides of that contract from C#.
This UXML is valid, compiles, and renders nothing:
<ui:ListView name="QuestList" fixed-item-height="40" />⚠️ Copy the UXML alone and you get an empty box. Unlike a
ScrollView, the ListView ignores any children you author in UXML — rows only exist once C# suppliesmakeItem/bindItem(or anitemTemplate) plus anitemsSource.
Rows only exist once C# supplies three things:
| You provide | Type | Role |
|---|---|---|
itemsSource | IList | The data. Required — without it the list is empty. |
makeItem | Func<VisualElement> | Builds one blank, reusable row. Called roughly once per visible row, not once per data item. |
bindItem | Action<VisualElement, int> | Copies the data item at index into a recycled row. Called every time a row scrolls into view or is refreshed. |
A minimal, complete example — UXML first:
<ui:ListView name="QuestList" fixed-item-height="40" selection-type="Single" />Then the C# that brings it to life (assuming you already have the panel's root element — see the Luna note below for the async caveat):
using System.Collections.Generic;
using UnityEngine.UIElements;
private readonly List<string> _quests = new()
{
"Find the herbalist",
"Clear the cellar",
"Deliver the letter",
};
private void InitQuestList(VisualElement root)
{
ListView listView = root.Q<ListView>("QuestList");
// 1. Build one blank row. No data here — this row will be reused
// for many different quests as the user scrolls.
listView.makeItem = () =>
{
var row = new VisualElement();
row.AddToClassList("quest-row");
row.Add(new Label());
return row;
};
// 2. Pour the data at `index` into a recycled row.
listView.bindItem = (row, index) =>
{
row.Q<Label>().text = _quests[index];
};
// 3. Hand over the data. The list builds itself from here.
listView.itemsSource = _quests;
}Instead of makeItem you can assign a .uxml row template to itemTemplate (UXML attribute item-template) — the ListView instantiates it for each pooled row. You still need bindItem to put data into the rows, unless the template uses runtime data bindings.
Because rows are recycled, never register data-dependent callbacks in makeItem. makeItem runs once per pool slot — a closure created there captures whatever data existed at creation time and goes stale after the first rebind. Register per-row callbacks (button clicks, value-changed) in bindItem, and unregister them in the mirror callback unbindItem:
listView.bindItem = (row, index) =>
{
Quest quest = _quests[index];
var btn = row.Q<Button>("Track");
btn.clicked += quest.Track; // bound to THIS quest
row.userData = (Action)quest.Track; // remember it for unbind
};
listView.unbindItem = (row, index) =>
{
var btn = row.Q<Button>("Track");
btn.clicked -= (Action)row.userData; // don't leak across reuse cycles
};Assigning a new itemsSource refreshes the list. If you mutate the existing list in place, the ListView doesn't notice — tell it:
_quests.Add("Slay the rat king");
listView.RefreshItems(); // rebinds all currently visible rowsRefreshItems() — rebinds visible rows. Use after item data or list contents change.Rebuild() — tears down and recreates all visible row elements, then rebinds. Use after structural changes: a new makeItem/itemTemplate, changed virtualization settings, or row-affecting class changes.virtualization-method controls how the ListView budgets rows:
| Mode | How it works | Trade-off |
|---|---|---|
FixedHeight (default) | Every row is exactly fixed-item-height pixels tall. Row count and scroll size are simple math. | Fastest. fixed-item-height is required — taller row content gets clipped to the default height without it. |
DynamicHeight | Each row is measured individually; rows can wrap and grow. | More flexible, less performant. fixed-item-height becomes just an initial estimate. |
<!-- uniform rows: save slots, menu entries -->
<ui:ListView name="LoadList" fixed-item-height="160" />
<!-- variable rows: quest text that wraps -->
<ui:ListView name="QuestsList" virtualization-method="DynamicHeight" />Rule of thumb: design rows to a fixed height if you can — only reach for DynamicHeight when content genuinely varies. And if you're tempted to use a plain ScrollView with hand-added children instead: that's fine for a handful of static elements, but it creates every child up front. ListView's recycling is what keeps a 10,000-row list as cheap as a 10-row one.
<ui:ListView name="LoadList" fixed-item-height="160" selection-type="Single" />selection-type — Single (default), Multiple, or None (display-only lists).selectedIndex — get/set the selected row's index into itemsSource.selectionChanged — event passing an IEnumerable<object> of selected data items.SetSelection(index) / ScrollToItem(index) — select and reveal programmatically.listView.selectionChanged += selected =>
{
foreach (object item in selected)
{
var quest = (Quest)item;
// show detail panel…
}
};Luna doesn't subclass ListView — it styles and orchestrates the built-in control:

coral, sky, violet, …) to the ListView element and the hover, selected-row, and scrollbar accents retint. The default ListView background is already transparent; a color class also adds a light (100-tone) background tint. See List & ScrollView for the class reference.ListViewController — an abstract MonoBehaviour base (entry template + list name in the Inspector) that handles the wiring above for you: override GetSourceList(), BindItem, UnbindItem, and OnSelectionChanged.ListViewWrapper — gamepad/keyboard navigation around any ListView: boundary focus hand-off to adjacent buttons, valid-selection-only events, auto-scroll on navigation.GridViewList<TItem> — a virtualized grid built on ListView, where each ListView row is a line of N item slots. See GridView.⚠️ Async warning (Luna): Luna's
PanelRendererbuilds its visual tree asynchronously —root.Q<ListView>(...)inAwake()returns null. If your script derives fromUIViewComponent, query and configure the ListView in theOnUILoaded(root)override, exactly as in First View. When not usingUIViewComponent, callPanelRenderer.RegisterUIReloadCallbackdirectly — Luna's ownListViewControllerand the GameFull top-barResourceChipBinderboth follow that pattern.
You can see all of this running in the imported samples: the Essentials Save/Load view (LoadList, fixed-height rows), the Notification history modal (selection-type="None"), and the Showcase Storybook ListView entry (color-variant switching).
GridViewList rides on ListView)bindItemSettings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction