Skip to content

UI Widgets

All widgets require Interactable and UIRect on the same entity. See UI Interaction for event handling.

Getting a Specific Widget

When you place multiple widgets in a scene, use Query to find a specific one.

Define a tag and attach it to the entity in the editor. Then use query.single() to get the one matching entity directly:

import { defineTag, defineSystem, addSystem, Query, Mut, ProgressBar } from 'esengine';
const HealthBar = defineTag('HealthBar');
addSystem(defineSystem(
[Query(Mut(ProgressBar), HealthBar)],
(query) => {
const [, bar] = query.single()!;
bar.value = playerHealth / maxHealth;
}
));

By Name

Every entity has a Name component set in the editor. Query it together with the widget component:

addSystem(defineSystem(
[Query(Mut(ProgressBar), Name)],
(query) => {
for (const [, bar, name] of query) {
if (name.value === 'HealthBar') bar.value = playerHealth / maxHealth;
if (name.value === 'ManaBar') bar.value = playerMana / maxMana;
if (name.value === 'ExpBar') bar.value = playerExp / expToNextLevel;
}
}
));

By findEntityByName + Direct Access

Use GetWorld() to access the World directly. findEntityByName() looks up an entity by its editor name, then world.get() / world.set() read/write components in O(1):

import { GetWorld, findEntityByName, ProgressBar } from 'esengine';
addSystem(defineSystem(
[GetWorld()],
(world) => {
const entity = findEntityByName(world, 'HealthBar');
if (entity) {
const bar = world.get(entity, ProgressBar);
bar.value = playerHealth / maxHealth;
world.set(entity, ProgressBar, bar);
}
}
));

Handling Events on a Specific Widget

With Tag + hasEvent

Combine query.single() to get the entity ID, then use UIEvents.hasEvent() to check directly — no loop:

import { defineTag, defineSystem, addSystem, Res, Query } from 'esengine';
import { UIEvents, Button } from 'esengine';
const StartButton = defineTag('StartButton');
addSystem(defineSystem(
[Res(UIEvents), Query(Button, StartButton)],
(events, query) => {
const match = query.single();
if (match && events.hasEvent(match[0], 'click')) {
startGame();
}
}
));

With UIInteraction (No UIEvents Needed)

For simple press/release checks, read the UIInteraction component directly:

import { UIInteraction } from 'esengine';
const StartButton = defineTag('StartButton');
addSystem(defineSystem(
[Query(UIInteraction, StartButton)],
(query) => {
const match = query.single();
if (match) {
const [, interaction] = match;
if (interaction.justPressed) startGame();
}
}
));

Matching Events by Name

When handling events from multiple widgets in one system:

addSystem(defineSystem(
[Res(UIEvents), Query(Button, Name)],
(events, query) => {
for (const e of events.query('click')) {
for (const [entity, , name] of query) {
if (entity === e.entity) {
console.log('Clicked:', name.value);
}
}
}
}
));

Button

State machine on top of Interactable with optional color transitions.

PropertyTypeDefaultDescription
stateButtonStateNormalNormal (0), Hovered (1), Pressed (2), Disabled (3)
transitionColorTransition | nullnullColor per state
cmds.entity(button).insert(Button, {
state: 0,
transition: {
normalColor: { r: 1, g: 1, b: 1, a: 1 },
hoveredColor: { r: 0.9, g: 0.9, b: 0.9, a: 1 },
pressedColor: { r: 0.7, g: 0.7, b: 0.7, a: 1 },
disabledColor: { r: 0.5, g: 0.5, b: 0.5, a: 0.5 },
},
});

Toggle

Checkbox with optional group for mutual exclusion.

PropertyTypeDefaultDescription
isOnbooleantrueToggle state
graphicEntityEntity0Child entity to show/hide (checkmark)
groupEntity0ToggleGroup entity for radio-button behavior
transitionColorTransition | nullnullColor transition
onColorColor{r:0.2, g:0.6, b:1, a:1}Color when on (used if no transition)
offColorColor{r:0.4, g:0.4, b:0.4, a:1}Color when off (used if no transition)

ToggleGroup — attach to a parent entity to make child toggles mutually exclusive:

PropertyTypeDefaultDescription
allowSwitchOffbooleanfalseAllow all toggles to be off

Listen for toggle changes:

addSystem(defineSystem(
[Res(UIEvents), Query(Toggle, Name)],
(events, query) => {
for (const e of events.query('change')) {
for (const [entity, toggle, name] of query) {
if (entity === e.entity) {
console.log(name.value, 'is now:', toggle.isOn);
}
}
}
}
));

Slider

Interactive value slider with a fill and optional draggable handle.

PropertyTypeDefaultDescription
valuenumber0Current value
minValuenumber0Minimum value
maxValuenumber1Maximum value
directionFillDirectionLeftToRightLeftToRight (0), RightToLeft (1), BottomToTop (2), TopToBottom (3)
fillEntityEntity0Child entity for fill visualization
handleEntityEntity0Child entity for draggable handle
wholeNumbersbooleanfalseSnap to integers

Listen for slider changes:

addSystem(defineSystem(
[Res(UIEvents), Query(Slider, Name)],
(events, query) => {
for (const e of events.query('change')) {
for (const [entity, slider, name] of query) {
if (entity === e.entity) {
if (name.value === 'VolumeSlider') setVolume(slider.value);
if (name.value === 'BrightnessSlider') setBrightness(slider.value);
}
}
}
}
));

ProgressBar

Non-interactive progress display.

PropertyTypeDefaultDescription
valuenumber0Progress 0–1
fillEntityEntity0Child entity for fill
directionFillDirectionLeftToRightFill direction

Dropdown list selector.

PropertyTypeDefaultDescription
optionsstring[][]Option labels
selectedIndexnumber-1Selected option index
isOpenbooleanfalseList visibility
listEntityEntity0Child entity for dropdown list
labelEntityEntity0Child entity for selected label

ScrollView

Scrollable container with momentum and elastic bounce.

PropertyTypeDefaultDescription
contentEntityEntity0Child holding scrollable content
horizontalEnabledbooleanfalseEnable horizontal scroll
verticalEnabledbooleantrueEnable vertical scroll
scrollXnumber0Current horizontal offset
scrollYnumber0Current vertical offset
inertiabooleantrueEnable momentum scrolling
decelerationRatenumber0.135Momentum friction
elasticbooleantrueBounce at edges
wheelSensitivitynumber0.1Mouse wheel amount
contentWidthnumber0Total width of scrollable content
contentHeightnumber0Total height of scrollable content

ListView

Virtual scrolling for large lists — only visible items are rendered.

PropertyTypeDefaultDescription
itemHeightnumber40Fixed item height
itemCountnumber0Total items
scrollYnumber0Scroll position
overscannumber2Extra items above/below viewport

Custom Item Renderer

Use setListViewRenderer() to define how each list item is populated. The callback receives the item index and the entity to configure:

import { setListViewRenderer, Text, Sprite } from 'esengine';
setListViewRenderer(listEntity, (index, itemEntity) => {
world.set(itemEntity, Text, { content: `Item ${index}`, fontSize: 16 });
});
ParameterTypeDescription
indexnumber0-based index of the list item
entityEntityThe spawned entity for this item — populate it with components

TextInput

Editable text field with cursor, selection, and placeholder.

PropertyTypeDefaultDescription
valuestring''Current text
placeholderstring''Hint when empty
placeholderColorColor{r:0.6, g:0.6, b:0.6, a:1}Placeholder color
fontFamilystring'Arial'Font family
fontSizenumber16Font size
colorColor{r:1, g:1, b:1, a:1}Text color
backgroundColorColor{r:0.15, g:0.15, b:0.15, a:1}Background color
paddingnumber6Internal padding
maxLengthnumber0Max characters (0 = unlimited)
multilinebooleanfalseAllow line breaks
passwordbooleanfalseMask text
readOnlybooleanfalsePrevent editing
focusedbooleanfalseWhether the input currently has focus
cursorPosnumber0Current cursor position in the text
dirtybooleantrueWhether the input needs re-rendering

Events: change (value changed), submit (Enter pressed in single-line mode)

Next Steps