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

Unity Documentation

The empty-box trap

This UXML is valid, compiles, and renders nothing:

html
<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# supplies makeItem/bindItem (or an itemTemplate) plus an itemsSource.

Rows only exist once C# supplies three things:

You provideTypeRole
itemsSourceIListThe data. Required — without it the list is empty.
makeItemFunc<VisualElement>Builds one blank, reusable row. Called roughly once per visible row, not once per data item.
bindItemAction<VisualElement, int>Copies the data item at index into a recycled row. Called every time a row scrolls into view or is refreshed.

The makeItem / bindItem contract

A minimal, complete example — UXML first:

html
<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):

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

The closure trap

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:

csharp
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 };

Refreshing the data

Assigning a new itemsSource refreshes the list. If you mutate the existing list in place, the ListView doesn't notice — tell it:

csharp
_quests.Add("Slay the rat king"); listView.RefreshItems(); // rebinds all currently visible rows
  • RefreshItems() — 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: fixed vs dynamic height

virtualization-method controls how the ListView budgets rows:

ModeHow it worksTrade-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.
DynamicHeightEach row is measured individually; rows can wrap and grow.More flexible, less performant. fixed-item-height becomes just an initial estimate.
html
<!-- 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.

Selection

html
<ui:ListView name="LoadList" fixed-item-height="160" selection-type="Single" />
  • selection-typeSingle (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.
csharp
listView.selectionChanged += selected => { foreach (object item in selected) { var quest = (Quest)item; // show detail panel… } };

How Luna uses ListView

Luna doesn't subclass ListView — it styles and orchestrates the built-in control:

List Component

  • Styling — Luna's theme restyles the stock ListView: add any color class (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 PanelRenderer builds its visual tree asynchronouslyroot.Q<ListView>(...) in Awake() returns null. If your script derives from UIViewComponent, query and configure the ListView in the OnUILoaded(root) override, exactly as in First View. When not using UIViewComponent, call PanelRenderer.RegisterUIReloadCallback directly — Luna's own ListViewController and the GameFull top-bar ResourceChipBinder both 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).

See also

  • List & ScrollView — Luna's USS styling for the built-in controls
  • GridView — Luna's higher-level grid wrapper (GridViewList rides on ListView)
  • Data Binding — the declarative alternative to bindItem

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