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 SerializedObject binding (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.

Unity Documentation

The three pieces

  1. A data source — any C# object, assigned to a 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.
  2. A bindable property — public fields are picked up automatically; C# properties need the [CreateProperty] attribute from the Unity.Properties namespace.
  3. A bindingelement.SetBinding(bindingId, binding) connects a target property on the element (e.g. "text", "value") to a path inside the data source.

Worked example: a player HUD

A name label and a health bar that track a PlayerStats object. First the data source:

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

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

  • Types must match. The binding system doesn't silently convert — a float source binds to a float UI property. For mismatches, register converters (type conversion docs).
  • Removing a binding: pass null to SetBinding, or bind a different DataBinding over it.

Binding modes

The default is TwoWay — for read-only HUD displays you usually want ToTarget:

ModeDirection
TwoWay (default)Data source ↔ UI. For input controls editing the source.
ToTargetData source → UI only. Read-only displays.
ToSourceUI → data source only.
ToTargetOnceData source → UI once, then stops (unless marked dirty). Static initialization.

Change detection (the performance fine print)

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.

Authoring bindings in UXML / UI Builder

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.

When classic queries + events are still simpler

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

  • One-shot reads and event-driven flows (a button click, a confirmation dialog) gain nothing from a live binding.
  • Anything animated or formatted — count-up tweens, "1,250" number formatting, conditional styling — quickly outgrows plain bindings and ends up in converter code; an event handler is often clearer.
  • Bindings fail quietly. A typo'd 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.

See also

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