Skip to content

User Interface

Build 2D panels – labels, sliders, buttons, stacks – from a script, place them in the scene or pin them to the screen, and drive them from your logic. For text that tracks a variable live, see Live labels below.

Build and place a panel

Compose a view tree with the UI.* builders, wrap it with createPanel(...), and instantiate it with scene.createEntity:

javascript
var panel = createPanel(
    UI.vStack({
        spacing: 12,
        children: [
            UI.label({ text: 'Volume' }),
            UI.slider({ name: 'vol', value: 0.5, range: [0, 1], sizing: { width: 'fill' } })
        ]
    }),
    { panelSize: [320, 160], material: 'system' }
).name('controls');

var entity = await scene.createEntity(
    panel.anchor(Anchor.screenSpace({ alignment: 'bottom', insets: 24 }))
);

Builders: UI.label, UI.button, UI.slider, UI.toggle, UI.textField, UI.progressView, UI.spacer, and the layout stacks UI.vStack / UI.hStack / UI.zStack. Every builder takes a name (for lookup), padding, and sizing ('auto' / 'fill' / a number / a '50%' string).

Anchor.screenSpace(...) pins the panel to a screen edge or corner; anchor it in the world instead (e.g. Anchor.position(...)) for a panel that lives in the scene.

Find and update views

Name a view, then reach it later through the object's representation to read or change it:

javascript
var view = entity.representation.findView({ name: 'vol' });
view.setProperty('value', 0.8);

setProperty / getProperty accept keys appropriate to the view kind (a label's text, a slider's value, a button's title, …). Setting a key the kind doesn't support is a no-op.

React to input

Subscribe to a view's events:

javascript
entity.representation.findView({ name: 'vol' }).on('valueChanged', function(e) {
    console.log('slider is now', e.uiValue);
});

entity.representation.findView({ name: 'save' }).on('tap', function() {
    scene.setVariable('saved', true);
});

Bind a control to a variable

bind connects a view's value to a variable in both directions – the control writes the variable as the user drags, and the view updates if the variable changes elsewhere:

javascript
entity.representation.findView({ name: 'vol' }).bind('value', 'volume');

Pair a bound control with a live label (below) to show its value.

Live labels

Label text supports inline variable references that resolve live – the label re-renders automatically whenever the referenced variable changes. No scene.on('variableChange', …) wiring needed.

javascript
UI.label({ text: 'vol = ${vol}' })

${vol} is read from the current segment's variable storage. If vol is unset, the label renders vol = UNDEFINED.

Scopes

FormReads from
${name}Segment-scoped variable (default)
${g:name}Experience-scoped (global) variable

Set scope on the variable side via scene.setVariable(name, value, { scope: 'experience' }).

Filters

Pipe the value through a formatter with |filter or |filter(arg):

FilterUseExample output (input 0.6789)
|fixed(N)Trim a float to N decimals (default 2)0.68
|percent(N)Multiply ×100, append % (default 0 decimals)68%
|timeSeconds → m:ss (or h:mm:ss ≥ 1h)(for 185) 3:05
|intTruncate toward zero0
|signed(N)Always-signed, N decimals (default 0)+1

Filters are numeric – applied to a non-numeric variable, they pass the raw value through unchanged. Unknown filter names log a console warning and do the same.

Example: bound slider with live readout

The slider drives the variable; the label reads from it – no subscription code, the label re-renders on every change automatically:

javascript
// Attach to: On Experience Start
scene.setVariable('vol', 0.5);

var panel = createPanel(
    UI.vStack({
        spacing: 12,
        padding: 16,
        children: [
            UI.label({ text: 'Volume: ${vol|fixed(2)}' }),
            UI.slider({ name: 'volume', value: 0.5 })
        ]
    }),
    { panelSize: [400, 160] }
);

var entity = await scene.createEntity(panel);
entity.representation.findView({ name: 'volume' }).bind('value', 'vol');

A single label can mix any number of references and plain text; each reference subscribes independently, so any one variable changing re-renders with the current values of all of them:

javascript
UI.label({ text: 'Score: ${g:score|int} ,  Timer: ${countdown|time}' })

Sizing panels with dynamic labels

A label re-measures itself when its value changes, so its own frame grows to fit wider text. But an auto-sized panel does not – it measures its outer bounds once, from the content present when it is first built. If you seed a label empty (or short) and fill it in later with a wider value, the panel stays at its original small size and the text truncates to .

For a panel whose interpolated content changes width at runtime – a live readout, a counter that gains digits, a place name – give the panel a fixed size rather than relying on auto-sizing:

javascript
createPanel(content, { panelSize: [340, 200] });

Fixed panels are unaffected: the labels have room to grow within them. Auto sizing is best kept for panels whose content width is known up front and does not change.