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:
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:
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:
<!-- 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:
<!-- 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-textwhen 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:
<!-- Shows: 12,500 gold -->
<span>{{player.gold|comma}} gold</span>
Native code defines formatters on the context using DefineFormat():
// 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):
<!-- 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:
<!-- 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:
<!-- Has the "low" class while lowHealth is true -->
<div class="portrait" ul-class-low="player.lowHealth"></div>
Stylesheets can then target the toggled class:
.portrait.low { border-color: crimson; }
🚧 Use Lowercase Names
Write the suffix on
ul-class-orul-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, soul-class-low="player.lowHealth"works as written.
Negating a Condition
Add a leading ! to negate a boolean field:
<!-- 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:
<!-- 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:
<!-- 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:
/* 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:
<!-- 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:
<!-- 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:
/* 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.