You can display native data on the page using `ul-*` attributes and `{{path}}` placeholders in markup. Calling `Sync()` in native code updates every bound element to match the latest values.

## Example Data

The examples on this page share a `Player` struct, which needs no schema because plain structs bind automatically under their member names— see [Describing Types with Schemas](/docs/2.0/describing-types-with-schemas):

```cpp
struct Stats {
  int health = 100;
  int armor = 0;
};

struct Player {
  std::string name;
  int level = 1;
  int64_t gold = 0;
  bool alive = true;
  bool inCombat = false;
  bool lowHealth = false;
  Stats stats;
};
```

Native code sets the initial values, binds the player under the name `"player"`, attaches the context to the view, and calls `Sync()` once per frame:

```cpp
Player player { .name = "Ava", .level = 12, .gold = 12500,
                .stats = { .health = 80, .armor = 12 } };

dd::Context ctx = dd::Context::Create();

// This instance will be exposed to markup as "player.<field>..."
dd::Binding binding = ctx.Bind("player", player);

if (!ctx.AttachTo(view.get()))
  Log("couldn't attach the data context");
view->LoadURL("file:///hud.html");

// Once per frame:
ctx.Sync();
```

## Paths

A path names a field for the page to show, using the binding name, a dot, and the field name:

```text
<binding name>.<field>                // Like "player.name"
```

The binding name is the name passed to `Bind()` in native code.

> 📘 Paths Only Name Data
>
> Paths cannot evaluate math or call functions. To compute values, calculate them in a formatter, in CSS, or in a schema accessor— see [Describing Types with Schemas](/docs/2.0/describing-types-with-schemas).

### Nested Fields

Each extra dot goes one level deeper into a nested object:

```text
<binding name>.<field>.<sub-field>    // Like "player.stats.armor"
```

## Displaying Text

To insert a value into existing text, place its path inside double curly braces:

```html
<!-- Shows: Welcome, Ava! -->
<h1>Welcome, {{player.name}}!</h1>
```

### Replacing an Element's Text

Use `ul-text` to replace an element's entire text content with a bound value:

```html
<!-- Shows "Loading..." until the data arrives, then "Ava" -->
<h1 ul-text="player.name">Loading...</h1>
```

> 👍 When to Use Placeholders vs ul-text?
>
> Use placeholders to insert values inside other text, combine several values, or use a formatter. Use `ul-text` when an element displays a single value— it shows its own text until data arrives instead of raw braces.

### How Values Appear

Values convert to text based on their native type:

| Field Type | What Appears | Example |
|---|---|---|
| `bool` | `true` or `false` | `true` |
| Integer | The number | `12500` |
| Floating-point | The number with all digits | `0.6666666666666666` |
| String | The text as written | `Ava` |
| Enum | The enumerator name | `Gold` |
| `Color` | Hex string | `#ff8000` (`#ff800080` when translucent) |

A `float` field displays with float precision (`0.8f` shows `0.8`), while a `double` field displays every digit. To round either number, use a formatter— see the next subsection.

### Formatting Values

To format a value, place the formatter name after a vertical bar inside a placeholder:

```html
<!-- Shows: 12,500 gold -->
<span>{{player.gold|comma}} gold</span>
```

Native code defines formatters on the context using `DefineFormat()`:

```cpp
// Formats 12500 as "12,500".
ctx.DefineFormat("comma", [](dd::Value v) {
  std::string text = std::to_string(v.Or(int64_t(0)));
  for (int i = int(text.size()) - 3; i > 0; i -= 3)
    text.insert(i, ",");
  return text;
});
```

Formatters accept `bool`, numeric, and string fields. When markup names an unknown formatter, the page displays the unformatted value and logs a warning.

> 🚧 Formatters Run on the Renderer's Thread
>
> Formatters must not read or modify native objects. A formatter must be deterministic and return the same text for the same value.

## Setting Attributes

Prefix an attribute name with `ul-attr:` to set its value— `{{path}}` placeholders work only in element text, never inside an attribute value (and stay literal inside `<script>`, `<style>`, and `<textarea>` elements):

```html
<!-- Sets value="80" -->
<progress max="100" ul-attr:value="player.stats.health"></progress>
```

`ul-attr:` refuses `class`, `style`, `slot`, `is`, `popover`, and every `on*` attribute except `open`. Use `ul-class-` to toggle classes and `ul-style-` to set inline styles.

### True/False Attributes

A `bool` field adds the attribute when true and removes it when false:

```html
<!-- Disabled while inCombat is true -->
<button ul-attr:disabled="player.inCombat">Save Game</button>
```

## Adding Classes

Prefix a class name with `ul-class-` to add that class while a `bool` field is true and remove it while false:

```html
<!-- Has the "low" class while lowHealth is true -->
<div class="portrait" ul-class-low="player.lowHealth"></div>
```

Stylesheets can then target the toggled class:

```css
.portrait.low { border-color: crimson; }
```

> 🚧 Use Lowercase Names
>
> Write the suffix on `ul-class-` or `ul-var-` in lowercase with hyphens between words. Browsers convert attribute names to lowercase, so a camelCase class will not match stylesheets on the page. Attribute values keep their case, so `ul-class-low="player.lowHealth"` works as written.

### Negating a Condition

Add a leading `!` to negate a boolean field:

```html
<!-- Has the "faded" class while alive is false -->
<div class="portrait" ul-class-faded="!player.alive"></div>
```

The `!` prefix works with `ul-class-`, `ul-show`, and `ul-if`, but `ul-attr:` rejects it.

## Setting Styles

Prefix a CSS property name with `ul-style-` to set that inline style:

```html
<!-- Sets width: 80% -->
<div class="health-bar" ul-style-width="player.stats.health|pct"></div>
```

Use the CSS property name (`ul-style-background-color`) instead of the JavaScript name.

When binding a numeric field to a length or angle property, append a unit pipe to the path: `px`, `pct` (which gives `%`), `em`, `rem`, `vw`, `vh`, `deg`, `ms`, or `s`. Unitless properties (eg, `opacity`) and CSS variables take plain numbers.

If a property refuses a value, the library drops the update, keeps the existing value, and logs a warning.

A `dom::StyleValue` or `Color` field already has its unit— a string field sets any CSS text (eg, `2px solid gold`).

## Setting CSS Variables

Prefix a custom property name with `ul-var-` to set a CSS variable on the element:

```html
<!-- Sets --armor: 12 on the element -->
<div class="shield" ul-var-armor="player.stats.armor"></div>
```

Stylesheets can then reference the variable on that element or any of its descendants:

```css
/* More armor, more visible shield. */
.shield { opacity: calc(var(--armor) / 100); }
```

Unit pipes work with `ul-var-` attributes as well. Schemas can also publish CSS variables directly from native code without markup using `dd::Var`— see [Describing Types with Schemas](/docs/2.0/describing-types-with-schemas).

## Showing and Hiding

Use `ul-show` to show the element while a `bool` field is true and hide it (`display: none`) while false:

```html
<!-- Visible while alive is true -->
<div class="hud" ul-show="player.alive"></div>
```

### Creating and Removing Elements

Use `ul-if` to insert an element into the page when true and remove it when false:

```html
<!-- Exists only while alive is false -->
<div class="respawn" ul-if="!player.alive">Respawning...</div>
```

While `ul-show` retains the element and its internal state in the DOM, `ul-if` removes it entirely and recreates it from its original markup each time the condition becomes true.

### Optional Objects as Conditions

An `std::optional` or pointer field also serves as a condition. It evaluates to true while populated and false while null or empty— see [Describing Types with Schemas](/docs/2.0/describing-types-with-schemas).

## Hiding the Page Until Data Arrives

Before the first update, the page displays its initial markup such as raw `{{player.name}}` placeholders and default `ul-text` content. Ultralight adds the `ul-ready` class to the root element once data arrives, so you can keep the page hidden until values are ready:

```css
/* Hide the page until the first update arrives. */
:root:not(.ul-ready) body { visibility: hidden; }
```

## Finding Markup Mistakes

A mistake in markup (eg, a typo in a path) doesn't stop the page. The renderer drops the invalid part and binds the rest of the markup.

To see warnings for these mistakes, turn on developer mode— see [Developer Mode and Diagnostics](/docs/2.0/developer-mode-and-diagnostics). Warnings go to the native `Logger` and to the page's console, so you can inspect them in the Web Inspector.

## Markup at a Glance

| Markup | What It Does |
|---|---|
| `{{path}}` | Inserts the value inside text |
| `ul-text` | Sets the element's whole text |
| `ul-attr:name` | Sets an attribute |
| `ul-class-name` | Adds a class while true |
| `ul-style-property` | Sets an inline style |
| `ul-var-name` | Sets the CSS variable `--name` on the element |
| `ul-show` | Shows the element while true |
| `ul-if` | Creates the element while true |
| `ul-for` | Repeats a `<template>` once per row— see [Binding Lists](/docs/2.0/binding-lists) |
| `ul-on:event` | Fires an action on an event— see [Handling Actions](/docs/2.0/handling-actions) |
| `ul-value` | Shows a field in a form control and sends changes back— see [Binding Form Controls](/docs/2.0/binding-form-controls) |

> 📘 Main Frame Only
>
> Data bindings operate only within a View's main frame. Markup inside `<iframe>` elements is not bound.
