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

CollectionView

Virtual scrolling for large datasets with pluggable layout providers and item pooling. Replaces the previous ListView.

PropertyTypeDefaultDescription
itemCountnumber0Total items in the dataset
layoutstring'linear'Layout provider: 'linear', 'grid', or 'fan'
virtualizedbooleantrueEnable virtual scrolling (only render visible items)
overscannumber2Extra items above/below viewport
itemPrefabstring''Prefab path for item template

Layout Providers

LayoutDescriptionKey Config
LinearVertical or horizontal listdirection, spacing
GridFixed-column gridcolumns, rowGap, columnGap
FanRadial fan arrangementradius, startAngle, endAngle

Custom Item Renderer

Use setCollectionAdapter() to define how items are created and populated:

import { setCollectionAdapter, Text } from 'esengine';
setCollectionAdapter(collectionEntity, {
makeItem(entity, world) {
// Called once when a new item entity is created
world.insert(entity, Text, { content: '', fontSize: 16 });
},
bindItem(entity, index, world) {
// Called each time an item is bound to a data index
const text = world.get(entity, Text);
text.content = `Item ${index}`;
world.set(entity, Text, text);
},
});

Items are automatically pooled — recycled items call bindItem with new indices, so the number of active entities stays minimal regardless of dataset size.

Selectable

Enables selection behavior with group-based mutual exclusion. When an entity in a group is selected, all other entities in the same group are automatically deselected.

PropertyTypeDefaultDescription
selectedbooleanfalseSelection state
groupnumber0Group ID for mutual exclusion (entities with the same group ID are mutually exclusive)

Listen for selection changes:

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

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