Data-Binding Basics: Values, Styles, and Classes
This article covers the core data-binding syntax in Gameface and walks through the most common binding types: data-bind-value for text content, data-bind-style-* for inline CSS properties, data-bind-class for discrete class names derived from model values, and data-bind-class-toggle for boolean class toggles.
A game HUD often tracks dozens of dynamic values at once: health, ammo, level, character name, buff timers, status indicators, and more. Without data-binding, each field typically needs its own imperative DOM update:
// SolidJS - no manual DOM queries needed. Signals drive the template.import { createSignal } from 'solid-js';import TextBlock from '@components/Basic/TextBlock/TextBlock';import Block from '@components/Layout/Block/Block';
const [health, setHealth] = createSignal(100);const [name, setName] = createSignal('');const [level, setLevel] = createSignal(1);const [healthPercent, setHealthPercent] = createSignal(100);
const HUD = () => ( <Block> <TextBlock>{health()}</TextBlock> <TextBlock>{name()}</TextBlock> <TextBlock>{level()}</TextBlock> <Block style={{ width: `${healthPercent()}%` }} class="health-bar-fill" /> </Block>);// For every value that changes, you write code like this...document.getElementById("health").textContent = player.health;document.getElementById("name").textContent = player.name;document.getElementById("level").textContent = player.level;document.getElementById("healthBar").style.width = player.healthPercent + "%";
// ... and you must remember to call this every time the data changes.Each new field adds another selector, update call, and failure point. Gameface’s data-binding system declares the relationship in
HTML with data-bind-* attributes, and the engine keeps the DOM aligned with the model when you call engine.synchronizeModels().
If you are not yet familiar with what a model is or how to create one in JavaScript, review the
Mocking Data Models article first. This article assumes you
already know how to use engine.createJSModel and engine.synchronizeModels.
The Double-Curly-Brace Syntax
Section titled “The Double-Curly-Brace Syntax”Gameface data-binding expressions use double-curly-brace syntax: {{Model.property}}. When the model updates and
you call synchronizeModels(), the engine re-evaluates bound expressions and applies the new values to the DOM.
The simplest form references a single property:
<span data-bind-value="{{PlayerModel.name}}"></span>Nested properties use dot notation:
<span data-bind-value="{{PlayerModel.pet.name}}"></span>Expressions and the Fast Path
Section titled “Expressions and the Fast Path”Expressions also support arithmetic, comparisons, and string concatenation:
<!-- Arithmetic: display health as a ratio --><span data-bind-value="{{player.health}} / {{player.maxHealth}}"></span>
<!-- String concatenation: append a unit --><div data-bind-style-width="{{player.healthPercent}} + '%'"></div>
<!-- Rounding: use toFixed to control decimal places --><span data-bind-value="{{player.speed}}.toFixed(1)"></span>Behind the scenes, Gameface evaluates these expressions through a tiered system. Understanding this is important for performance:
-
Native evaluation (fastest): Expressions that reference a single model property (e.g.,
{{player.health}}) are resolved entirely by the SDK with no JavaScript involvement. This is the most efficient path. -
SDK-evaluated expressions (fast): Arithmetic, boolean operations, and a small set of built-in functions are still handled natively by the SDK without falling back to JavaScript. The supported built-in functions are:
toFixed()Math.floor()Math.round()Math.ceil()Math.abs()
-
JavaScript fallback (slower): Any expression that uses JavaScript objects or functions beyond the built-in set above is evaluated through a JavaScript fallback. This is functionally correct but carries additional overhead per evaluation.
Array Access
Section titled “Array Access”Array indexing inside {{ }} requires a numeric literal index:
<!-- Valid: literal index --><span data-bind-value="{{model.items[0].name}}"></span><span data-bind-value="{{model.items[2].price}}"></span>
<!-- Invalid: variable or dynamic indices inside {{ }} --><span data-bind-value="{{model.items[myVar].name}}"></span><span data-bind-value="{{model.items[{{otherModel.idx}}].name}}"></span>Dynamic indices belong outside the curly braces so the expression uses JavaScript evaluation:
<span data-bind-value="{{model.items}}[dynamicIndex].name"></span>Binding Text Content
Section titled “Binding Text Content”Gameface exposes two attributes for dynamic text, differing by how the bound string is applied to the element.
data-bind-value
Section titled “data-bind-value”The data-bind-value attribute takes the result of its expression and assigns it to the element’s textContent. This is the
standard way to display plain text values such as player names, health numbers, ammo counts and other text-based values.
Consider this nameplate snippet that displays a character’s level and name:
import Block from '@components/Layout/Block/Block';import InlineTextBlock from '@components/Basic/InlineTextBlock/InlineTextBlock';import TextBlock from '@components/Basic/TextBlock/TextBlock';
const NameplateHeader = () => ( <Block class="nameplate__header"> <InlineTextBlock class="nameplate__level"> LV. <span data-bind-value="{{EnemyModel.level}}" /> </InlineTextBlock> <TextBlock class="nameplate__name" attr:data-bind-value="{{EnemyModel.name}}" /> </Block>);<div class="nameplate__header"> <!-- Display the character's level --> <div class="nameplate__level">LV. <span data-bind-value="{{EnemyModel.level}}"></span></div>
<!-- Display the character's name --> <span class="nameplate__name" data-bind-value="{{EnemyModel.name}}"></span></div>With { level: 42, name: "Cyber Ninja" }, the bound spans render 42 and Cyber Ninja beside the static LV. prefix.
<span data-bind-value="{{player.speed}}.toFixed(1)"></span>data-bind-html
Section titled “data-bind-html”The data-bind-html attribute writes to innerHTML, so the bound string can include markup (formatting, colors, inline images).
Tooltips are a typical use:
<div class="tooltip__body" data-bind-html="{{item.tooltipHTML}}"></div>If the model property contains a string like "<b>Fire Sword</b> <span style='color: #ff4444;'>+25 damage</span>", the element will render the bold
text and the colored span as actual HTML elements.
Binding Inline Styles
Section titled “Binding Inline Styles”Beyond text content, data-binding can drive CSS properties directly. The data-bind-style-* family of attributes lets you bind
model values to specific inline styles on an element. The naming convention is straightforward: data-bind-style- followed by the CSS property name.
Supported Style Attributes
Section titled “Supported Style Attributes”Gameface provides dedicated, optimized bindings for the most commonly used style properties:
| Attribute | CSS Property | Accepted Values |
|---|---|---|
data-bind-style-left | left | String or number (defaults to px) |
data-bind-style-top | top | String or number (defaults to px) |
data-bind-style-width | width | String or number (defaults to px) |
data-bind-style-height | height | String or number (defaults to px) |
data-bind-style-opacity | opacity | Floating point number between 0 and 1 |
data-bind-style-color | color | CSS color string or unsigned RGBA |
data-bind-style-background-color | background-color | CSS color string or unsigned RGBA |
data-bind-style-background-image-url | background-image | URL to the image |
data-bind-style-transform-rotate | transform: rotate(..) | String or number (defaults to deg) |
data-bind-style-transform2d | transform | String of 6 comma-separated numbers |
Beyond this core set, Gameface’s dynamic data binding extends support to every CSS property the engine supports. You can bind any supported property
using the pattern data-bind-style-PROPERTYNAME:
<!-- Binding to properties beyond the core set --><div data-bind-style-font-size="{{settings.fontSize}}"></div><div data-bind-style-border-radius="{{card.cornerRadius}}"></div>The Default Unit Rule
Section titled “The Default Unit Rule”An important mechanic to internalize: when you bind a raw number (not a string) to a style property that expects a length, the engine assumes the unit is pixels .
For example, if EnemyModel.x is 150, then:
<div data-bind-style-left="{{EnemyModel.x}}"></div><!-- Equivalent to: style="left: 150px" -->This applies to left, top, width, height, and any other length-based property. The same rule applies to data-bind-style-transform-rotate,
where a raw number is interpreted as degrees (deg) .
If you need a different unit (like percentages), you must produce a string that includes the unit:
<!-- Concatenate the '%' sign to produce a string like "65%" --><div data-bind-style-width="{{EnemyModel.healthPercent}} + '%'"></div>Practical usages
Section titled “Practical usages”Let’s cover two practical usages of style binding: driving a percentage width and pinning an element to screen coordinates.
The fill element’s width tracks health as a percentage:
import Block from '@components/Layout/Block/Block';
const HealthBar = () => ( <Block class="nameplate__health-track"> <Block class="nameplate__health-fill" attr:data-bind-style-width="{{EnemyModel.healthPercent}} + '%'" /> </Block>);<!-- Health bar track (the dark background) --><div class="nameplate__health-track"> <!-- Health bar fill (the colored bar) --> <div class="nameplate__health-fill" data-bind-style-width="{{EnemyModel.healthPercent}} + '%'"></div></div>And the accompanying CSS:
.nameplate__health-track { width: 100%; height: 8px; background-color: #0a0a0a; border-radius: 1px; overflow: hidden;}
.nameplate__health-fill { height: 100%; background-color: #00dcff; /* Smooth visual transitions when the value changes */ transition: width 0.1s linear;}With { healthPercent: 65 }, the binding resolves to width: 65%. A CSS transition on width animates between updates the same way as a manual
style assignment.
World-attached UI (nameplates, floating damage, follow cursors) usually receives screen coordinates directly from the game engine:
<div class="nameplate" data-bind-style-left="{{EnemyModel.x}}" data-bind-style-top="{{EnemyModel.y}}"> <!-- Nameplate content goes here --></div>Since EnemyModel.x and EnemyModel.y are numbers, the engine automatically appends px. The nameplate follows the character’s screen position as
the model updates each frame.
Binding CSS Classes
Section titled “Binding CSS Classes”Discrete states (rarity tiers, alive/dead, selected buttons) map more cleanly to CSS classes than to per-property style bindings. Gameface exposes
data-bind-class for dynamic class names and data-bind-class-toggle for boolean toggles.
data-bind-class
Section titled “data-bind-class”The data-bind-class attribute sets class names from a string expression.
Inventory rarity styling:
.rarity-common { border-color: #888888;}.rarity-rare { border-color: #4488ff;}.rarity-epic { border-color: #aa44ff;}.rarity-legendary { border-color: #ff8800;}<!-- If item.rarity is "rare", this adds the class "rarity-rare" --><div class="inventory-slot" data-bind-class="'rarity-' + {{item.rarity}}"></div>The expression 'rarity-' + {{item.rarity}} concatenates the static prefix with the model value. If item.rarity is "legendary", the element
receives the class rarity-legendary.
To bind multiple dynamic classes on one element, separate each expression with a semicolon:
<!-- Two dynamic classes from different model properties --><div data-bind-class="'type-' + {{item.type}}; 'rarity-' + {{item.rarity}}"></div>data-bind-class-toggle
Section titled “data-bind-class-toggle”The data-bind-class-toggle attribute adds a class when a condition is true and removes it when false. Syntax: class-name: condition
Conditions may be boolean model properties or comparisons. Multiple toggles use semicolons, same as data-bind-class.
<div data-bind-class-toggle="isDead: {{Model.player.health}} <= 0"></div>Practical Example: Flickering Health Bar
Section titled “Practical Example: Flickering Health Bar”Extend the health bar so it turns red and pulses below a threshold:
.nameplate__health-fill { height: 100%; background-color: #00dcff; transition: width 0.1s linear;}
/* Applied when health drops below the threshold */.health-critical { background-color: #ff2222; animation: pulse-critical 0.8s ease-in-out infinite;}
@keyframes pulse-critical { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; }}Bind the toggle on the fill element:
import Block from '@components/Layout/Block/Block';
const CriticalHealthFill = () => ( <Block class="nameplate__health-fill" attr:data-bind-style-width="{{EnemyModel.healthPercent}} + '%'" attr:data-bind-class-toggle="health-critical: {{EnemyModel.healthPercent}} < {{EnemyModel.classThreshold}}" />);<div class="nameplate__health-fill" data-bind-style-width="{{EnemyModel.healthPercent}} + '%'" data-bind-class-toggle="health-critical: {{EnemyModel.healthPercent}} < {{EnemyModel.classThreshold}}"></div>When EnemyModel.healthPercent falls below EnemyModel.classThreshold, the engine applies health-critical (red fill and pulse).
Above the threshold, the class is removed.
Notice how the class is added and then later removed when the condition is no longer met.
You can also use a direct boolean property instead of an inline comparison:
<!-- Using a pre-computed boolean from the model --><div class="nameplate__health-fill" data-bind-class-toggle="health-critical: {{EnemyModel.isCritical}}"></div>To apply multiple toggles on one element, separate each condition with a semicolon:
<div data-bind-class-toggle="health-critical: {{Model.isCritical}}; poisoned: {{Model.isPoisoned}}"></div>Complete Example
Section titled “Complete Example”Throughout this article, each section has covered a different binding type in isolation. Now, let us combine them into a single, complete component:
an enemy nameplate that displays a name, level, a rank badge whose art comes from a discrete rank string, a health bar with a critical state, and
follows screen coordinates provided by the game.
The Model for this nameplate would look like this:
const EnemyModel = { name: "Cyber Ninja", level: 42, // "normal" | "elite" | "boss" rank: "elite", healthPercent: 65, classThreshold: 25, x: 200, y: 100,};Here is the full HTML structure using every binding type covered in this article:
import Absolute from '@components/Layout/Absolute/Absolute';import Block from '@components/Layout/Block/Block';import InlineTextBlock from '@components/Basic/InlineTextBlock/InlineTextBlock';import TextBlock from '@components/Basic/TextBlock/TextBlock';
const Nameplate = () => ( <Absolute class="nameplate" attr:data-bind-style-left="{{EnemyModel.x}}" attr:data-bind-style-top="{{EnemyModel.y}}" > <Block class="nameplate__inner"> <Block class="nameplate__header"> <Block class="nameplate__identity"> <InlineTextBlock class="nameplate__level"> LV. <span data-bind-value="{{EnemyModel.level}}" /> </InlineTextBlock> <TextBlock class="nameplate__name" attr:data-bind-value="{{EnemyModel.name}}" /> </Block> <Block class="nameplate__elite-icon" attr:data-bind-class="'nameplate__elite-icon--' + {{EnemyModel.rank}}" /> </Block> <Block class="nameplate__health-track"> <Block class="nameplate__health-fill" attr:data-bind-style-width="{{EnemyModel.healthPercent}} + '%'" attr:data-bind-class-toggle="health-critical: {{EnemyModel.healthPercent}} < {{EnemyModel.classThreshold}}" /> </Block> </Block> </Absolute>);
export default Nameplate;<div class="nameplate" data-bind-style-left="{{EnemyModel.x}}" data-bind-style-top="{{EnemyModel.y}}"> <div class="nameplate__inner"> <!-- Top row: Level, Name, and Elite indicator --> <div class="nameplate__header"> <div class="nameplate__identity"> <div class="nameplate__level">LV. <span data-bind-value="{{EnemyModel.level}}"></span></div> <span class="nameplate__name" data-bind-value="{{EnemyModel.name}}"></span> </div> <!-- Rank badge: modifier class is built from the rank string --> <div class="nameplate__elite-icon" data-bind-class="'nameplate__elite-icon--' + {{EnemyModel.rank}}"></div> </div>
<!-- Health bar with critical state toggle --> <div class="nameplate__health-track"> <div class="nameplate__health-fill" data-bind-style-width="{{EnemyModel.healthPercent}} + '%'" data-bind-class-toggle="health-critical: {{EnemyModel.healthPercent}} < {{EnemyModel.classThreshold}}"></div> </div> </div></div>The CSS that styles the nameplate and defines the critical health state.
.nameplate { position: absolute; width: 280px; background-color: rgba(20, 20, 20, 0.85); border: 1px solid #333333; border-bottom: 3px solid #00dcff; transform: skewX(-12deg); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.6); transition: left 0.1s linear, top 0.1s linear;}
.nameplate__inner { /* Counter-skew to keep text readable */ transform: skewX(12deg); padding: 10px 14px;}
.nameplate__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;}
.nameplate__identity { display: flex; align-items: center;}
.nameplate__level { font-family: "Karla", sans-serif; font-size: 0.9rem; font-weight: bold; color: #00dcff; margin-right: 8px; display: flex;}
.nameplate__name { font-family: "Barlow Semi Condensed", sans-serif; font-size: 1.2rem; font-weight: bold; color: #ffffff; text-transform: uppercase; letter-spacing: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 160px;}
.nameplate__elite-icon { flex-shrink: 0; width: 16px; height: 16px;}
.nameplate__elite-icon--normal { display: none;}
.nameplate__elite-icon--elite { background-color: #f5bc35; clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); box-shadow: 0 0 10px rgba(245, 188, 53, 0.5);}
.nameplate__elite-icon--boss { width: 20px; height: 20px; background-color: #c44cff; clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%); box-shadow: 0 0 12px rgba(196, 76, 255, 0.55);}
.nameplate__health-track { width: 100%; height: 8px; background-color: #0a0a0a; border-radius: 1px; overflow: hidden;}
.nameplate__health-fill { height: 100%; background-color: #00dcff; transition: width 0.1s linear;}
.health-critical { background-color: #ff2222; animation: pulse-critical 0.8s ease-in-out infinite;}
@keyframes pulse-critical { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; }}Finally, the JavaScript that creates the mock model and drives the data-binding loop. If you are unfamiliar with createJSModel or the
synchronization pattern below, refer to the Mocking Data Models
article:
engine.whenReady.then(() => { // Create the model that the nameplate binds to engine.createJSModel("EnemyModel", { name: "Cyber Ninja", level: 42, rank: "elite", healthPercent: 65, classThreshold: 25, x: 200, y: 100, });
// Synchronize once to push the initial state to the DOM engine.synchronizeModels();
// Simulate data changes (for testing) setInterval(() => { if (EnemyModel.healthPercent > 0) { EnemyModel.healthPercent -= 1; }
EnemyModel.x += 3; EnemyModel.y += 1;
setTimeout(() => { EnemyModel.rank = "boss"; }, 2000);
// Update the model and synchronize the changes to the DOM engine.updateWholeModel(EnemyModel); engine.synchronizeModels(); }, 100);});This single component uses five different binding types working together:
data-bind-valuedisplays the character name and level as text.data-bind-style-leftanddata-bind-style-topposition the nameplate on screen.data-bind-classmaps therankstring to a badge modifier class (nameplate__elite-icon--elite, and so on).data-bind-style-widthdrives the health bar fill.data-bind-class-toggleswitches the bar to a red pulsing state when health is critical.
All of this runs declaratively. The JavaScript only updates the model’s properties and calls synchronizeModels(). The engine handles everything
else.
© 2026 Coherent Labs. All rights reserved.