docs

Showing Data in Markup

Display native data in page markup using attributes, text placeholders, and CSS variables.

On this page

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:

C++
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:

C++
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:

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

Nested Fields

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

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

C++
// 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.

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.

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. 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
ul-on:event Fires an action on an event— see Handling Actions
ul-value Shows a field in a form control and sends changes back— see Binding Form Controls

📘 Main Frame Only

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