Unity 6 ships a runtime data binding system for UI Toolkit: declare a connection between a property on any plain C# object and a property on a UI control, and the binding system keeps them in sync — no Q<> lookups, no event subscriptions, no per-frame label.text = ... updates in your own code.
Not to be confused with the older editor-only
SerializedObjectbinding (bindingPath+Bind()), which only works against serialized Unity objects in Editor UI. This page is about the runtime system, which binds to any C# object in a built game.
VisualElement's dataSource property. Elements without their own dataSource resolve it from the nearest ancestor, so setting it once on a panel root covers the whole subtree.[CreateProperty] attribute from the Unity.Properties namespace.element.SetBinding(bindingId, binding) connects a target property on the element (e.g. "text", "value") to a path inside the data source.A name label and a health bar that track a PlayerStats object. First the data source:
using System;
using Unity.Properties;
using UnityEngine.UIElements;
public class PlayerStats : INotifyBindablePropertyChanged
{
public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;
private string _playerName;
private float _health;
[CreateProperty]
public string PlayerName
{
get => _playerName;
set { _playerName = value; Notify(nameof(PlayerName)); }
}
[CreateProperty]
public float Health // 0..100, matches the ProgressBar range
{
get => _health;
set { _health = value; Notify(nameof(Health)); }
}
private void Notify(string property) =>
propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
}Then the wiring — set the source once on the root, then declare one binding per control:
using Unity.Properties;
using UnityEngine.UIElements;
var stats = new PlayerStats { PlayerName = "Aldric", Health = 80f };
root.dataSource = stats; // inherited by every descendant
root.Q<Label>("PlayerName").SetBinding("text", new DataBinding
{
dataSourcePath = new PropertyPath(nameof(PlayerStats.PlayerName)),
bindingMode = BindingMode.ToTarget,
});
root.Q<ProgressBar>("HealthBar").SetBinding("value", new DataBinding
{
dataSourcePath = new PropertyPath(nameof(PlayerStats.Health)),
bindingMode = BindingMode.ToTarget,
});From now on, stats.Health = 25f; anywhere in gameplay code updates the bar. No reference to the UI, no event plumbing.
Two details worth noting:
float source binds to a float UI property. For mismatches, register converters (type conversion docs).null to SetBinding, or bind a different DataBinding over it.The default is TwoWay — for read-only HUD displays you usually want ToTarget:
| Mode | Direction |
|---|---|
TwoWay (default) | Data source ↔ UI. For input controls editing the source. |
ToTarget | Data source → UI only. Read-only displays. |
ToSource | UI → data source only. |
ToTargetOnce | Data source → UI once, then stops (unless marked dirty). Static initialization. |
If the data source is a plain object with no notification support, the binding system polls it every frame — it can't know whether values changed. That's fine for a handful of bindings; it adds up for many.
Two opt-in interfaces fix this:
INotifyBindablePropertyChanged (used above) — raise propertyChanged with the property name; only the affected bindings update.IDataSourceViewHashProvider — return a version hash for the whole source; the system skips updates while the hash is unchanged.Implement at least the first on any data source that lives for more than a frame.
Bindings can also be declared in UXML inside a <Bindings> block, or interactively in UI Builder (right-click a property field → Add binding). You've already seen this system in production if you read the Localization guide — Unity Localization's LocalizedString is a binding type plugged into this same <Bindings> mechanism, and that page walks through the UI Builder authoring flow step by step.
For ListView rows, binding-source-selection-mode="AutoAssign" makes each row receive its data item as dataSource, so a row template authored with bindings can replace hand-written bindItem code.
💡 Classic
Q<>+ events is often simpler. Data binding is not a default-on replacement — reach for it when you have many always-on data readouts, not for one-shot reads or event-driven flows.
"1,250" number formatting, conditional styling — quickly outgrows plain bindings and ends up in converter code; an event handler is often clearer.PropertyPath logs at most a binding-system warning, where a null Q<> result fails loudly at the call site.Luna itself is a data point here: its runtime is built almost entirely on direct queries + C# events (the GameFull top-bar gold/XP chips, for example, subscribe to wallet/experience events and tween label text — something a binding can't express). Luna touches the binding system in two places: LocalizedVisualElement calls SetBinding("text", localizedString) to attach Unity Localization bindings, and custom controls mark UI Builder-facing properties like the ProgressBar title with [CreateProperty] so you can bind them. Treat binding as a tool for many always-on data readouts — not a framework requirement.
makeItem/bindItem contract bindings can partially replaceLocalizedStringSettings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction