Skip to content
SiteEmail

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:

imperative-approach-solidjs.tsx
// 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>
);

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.

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

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

  2. 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()
  3. 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 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>

Gameface exposes two attributes for dynamic text, differing by how the bound string is applied to the element.

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:

Nameplate.tsx
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>
);

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>

The data-bind-html attribute writes to innerHTML, so the bound string can include markup (formatting, colors, inline images). Tooltips are a typical use:

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

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.

Gameface provides dedicated, optimized bindings for the most commonly used style properties:

AttributeCSS PropertyAccepted Values
data-bind-style-leftleftString or number (defaults to px)
data-bind-style-toptopString or number (defaults to px)
data-bind-style-widthwidthString or number (defaults to px)
data-bind-style-heightheightString or number (defaults to px)
data-bind-style-opacityopacityFloating point number between 0 and 1
data-bind-style-colorcolorCSS color string or unsigned RGBA
data-bind-style-background-colorbackground-colorCSS color string or unsigned RGBA
data-bind-style-background-image-urlbackground-imageURL to the image
data-bind-style-transform-rotatetransform: rotate(..)String or number (defaults to deg)
data-bind-style-transform2dtransformString 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>

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>

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:

HealthBar.tsx
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>
);

And the accompanying CSS:

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

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.

The data-bind-class attribute sets class names from a string expression.

Inventory rarity styling:

item-rarity.css
.rarity-common {
border-color: #888888;
}
.rarity-rare {
border-color: #4488ff;
}
.rarity-epic {
border-color: #aa44ff;
}
.rarity-legendary {
border-color: #ff8800;
}
inventory-item.html
<!-- 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>

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>

Extend the health bar so it turns red and pulses below a threshold:

nameplate.css
.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:

CriticalHealthBar.tsx
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}}"
/>
);

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>

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:

Nameplate.tsx
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;

The CSS that styles the nameplate and defines the critical health state.

nameplate.css
.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:

nameplate-mock.js
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-value displays the character name and level as text.
  • data-bind-style-left and data-bind-style-top position the nameplate on screen.
  • data-bind-class maps the rank string to a badge modifier class (nameplate__elite-icon--elite, and so on).
  • data-bind-style-width drives the health bar fill.
  • data-bind-class-toggle switches 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.