build-ui
Building UI with React-ECS
Decentraland SDK7 uses a React-like JSX system for 2D UI overlays.
When to Use Which UI Approach
| Need | Approach | Component |
|---|---|---|
| Screen-space HUD, menus, buttons | React-ECS (this skill) | UiEntity, Label, Button, Input, Dropdown |
| 3D text floating in the world | TextShape + Billboard | See advanced-rendering skill |
| Open a web page | openExternalUrl |
See scene-runtime skill |
| Clickable objects in 3D space | Pointer events | See add-interactivity skill |
Use React-ECS for any 2D overlay: scoreboards, health bars, dialogs, inventories, settings menus. Use TextShape for labels above NPCs or objects in the 3D world.
Setup
File: src/ui.tsx
import ReactEcs, { ReactEcsRenderer, UiEntity, Label, Button } from '@dcl/sdk/react-ecs'
const MyUI = () => (
<UiEntity
uiTransform={{
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center'
}}
>
<Label value="Hello Decentraland!" fontSize={24} />
</UiEntity>
)
export function setupUi() {
ReactEcsRenderer.setUiRenderer(MyUI)
}
File: src/index.ts
import { setupUi } from './ui'
export function main() {
setupUi()
}
tsconfig.json (already configured by /init)
The SDK template already includes the required JSX settings — do NOT modify tsconfig.json:
"jsx": "react-jsx""jsxImportSource": "@dcl/sdk/react-ecs-lib"
Core Components
UiEntity (Container)
import { Color4 } from '@dcl/sdk/math'
<UiEntity
uiTransform={{
width: 300, // Pixels or '50%'
height: 200,
positionType: 'absolute', // 'absolute' or 'relative' (default)
position: { top: 10, right: 10 }, // Only with absolute
flexDirection: 'column', // 'row' | 'column'
justifyContent: 'center', // 'flex-start' | 'center' | 'flex-end' | 'space-between'
alignItems: 'center', // 'flex-start' | 'center' | 'flex-end' | 'stretch'
padding: { top: 10, bottom: 10, left: 10, right: 10 },
margin: { top: 5 },
display: 'flex' // 'flex' | 'none' (hide)
}}
uiBackground={{
color: Color4.create(0, 0, 0, 0.8) // Semi-transparent black
}}
/>
Label (Text)
import { Color4 } from '@dcl/sdk/math'
<Label
value="Score: 100"
fontSize={18}
color={Color4.White()}
textAlign="middle-center"
font="sans-serif"
uiTransform={{ width: 200, height: 30 }}
/>
Button
<Button
value="Click Me"
variant="primary" // 'primary' | 'secondary'
fontSize={16}
uiTransform={{ width: 150, height: 40 }}
onMouseDown={() => {
console.log('Button clicked!')
}}
/>
Input
import { Input } from '@dcl/sdk/react-ecs'
import { Color4 } from '@dcl/sdk/math'
<Input
placeholder="Type here..."
fontSize={14}
color={Color4.White()}
uiTransform={{ width: 250, height: 35 }}
onChange={(value) => {
console.log('Value changing:', value)
}}
onSubmit={(value) => {
console.log('Submitted:', value)
}}
/>
Dropdown
import { Dropdown } from '@dcl/sdk/react-ecs'
<Dropdown
options={['Option A', 'Option B', 'Option C']}
selectedIndex={0}
onChange={(index) => {
console.log('Selected:', index)
}}
uiTransform={{ width: 200, height: 35 }}
fontSize={14}
/>
State Management
Use module-level variables for UI state (React hooks are NOT available):
import { Color4 } from '@dcl/sdk/math'
let score = 0
let showMenu = false
const GameUI = () => (
<UiEntity uiTransform={{ width: '100%', height: '100%' }}>
{/* HUD - always visible */}
<Label
value={`Score: ${score}`}
fontSize={20}
uiTransform={{
positionType: 'absolute',
position: { top: 10, left: 10 }
}}
/>
{/* Menu - conditionally shown */}
{showMenu && (
<UiEntity
uiTransform={{
width: 300,
height: 400,
positionType: 'absolute',
position: { top: '50%', left: '50%' }
}}
uiBackground={{ color: Color4.create(0.1, 0.1, 0.1, 0.9) }}
>
<Label value="Game Menu" fontSize={24} />
<Button
value="Resume"
variant="primary"
onMouseDown={() => { showMenu = false }}
uiTransform={{ width: 200, height: 40 }}
/>
</UiEntity>
)}
</UiEntity>
)
// Update state from game logic
export function addScore(points: number) {
score += points
}
export function toggleMenu() {
showMenu = !showMenu
}
Common UI Patterns
Health Bar
import { Color4 } from '@dcl/sdk/math'
let health = 100
const HealthBar = () => (
<UiEntity
uiTransform={{
width: 200, height: 20,
positionType: 'absolute',
position: { bottom: 20, left: '50%' }
}}
uiBackground={{ color: Color4.create(0.3, 0.3, 0.3, 0.8) }}
>
<UiEntity
uiTransform={{ width: `${health}%`, height: '100%' }}
uiBackground={{ color: Color4.create(0.2, 0.8, 0.2, 1) }}
/>
</UiEntity>
)
Image Background
<UiEntity
uiTransform={{ width: 200, height: 200 }}
uiBackground={{
textureMode: 'stretch',
texture: { src: 'images/logo.png' }
}}
/>
Screen Dimensions
Read screen size via UiCanvasInformation:
import { UiCanvasInformation } from '@dcl/sdk/ecs'
engine.addSystem(() => {
const canvas = UiCanvasInformation.getOrNull(engine.RootEntity)
if (canvas) {
console.log('Screen:', canvas.width, 'x', canvas.height)
}
})
Nine-Slice Textures
Use textureSlices for scalable UI backgrounds (buttons, panels) that don't stretch corners:
<UiEntity
uiTransform={{ width: 200, height: 100 }}
uiBackground={{
textureMode: 'nine-slices',
texture: { src: 'images/panel.png' },
textureSlices: { top: 0.1, bottom: 0.1, left: 0.1, right: 0.1 }
}}
/>
Hover Events
Respond to mouse enter/leave for hover effects:
<UiEntity
uiTransform={{ width: 100, height: 40 }}
onMouseEnter={() => { isHovered = true }}
onMouseLeave={() => { isHovered = false }}
uiBackground={{ color: isHovered ? Color4.White() : Color4.Gray() }}
/>
Flex Wrap
Allow UI children to wrap to the next line:
<UiEntity uiTransform={{ flexWrap: 'wrap', width: 300 }}>
{items.map(item => (
<UiEntity key={item.id} uiTransform={{ width: 80, height: 80, margin: 4 }} />
))}
</UiEntity>
Dropdown Extras
The Dropdown component supports additional props:
<Dropdown
options={['Option A', 'Option B', 'Option C']}
selectedIndex={selectedIdx}
onChange={(idx) => { selectedIdx = idx }}
fontSize={14}
color={Color4.White()}
disabled={false}
/>
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| UI not appearing at all | Missing ReactEcsRenderer.setUiRenderer() call |
Add ReactEcsRenderer.setUiRenderer(MyUI) in main() or setupUi() |
| UI elements overlapping | Missing flexDirection or wrong layout |
Set flexDirection: 'column' on the parent container |
| Button clicks not registering | Missing onMouseDown handler |
Add onMouseDown={() => { ... }} to the Button or UiEntity |
| JSX errors at compile time | File extension is .ts instead of .tsx |
Rename the file to .tsx |
| Multiple UIs fighting | More than one setUiRenderer call |
Only call setUiRenderer once — combine all UI into a single root component |
| Text not visible | Text color matches background | Set contrasting color on Label or uiText |
World interactions instead of screen UI? See the add-interactivity skill for click handlers and pointer events on 3D objects.
Important Notes
- React hooks (
useState,useEffect, etc.) are NOT available — use module-level variables - The UI renderer re-renders every frame, so state changes are reflected immediately
- UI is rendered as a 2D overlay on top of the 3D scene
- Use
display: 'none'inuiTransformto hide elements without removing them - File extension must be
.tsxfor JSX support - Only one
ReactEcsRenderer.setUiRenderer()call per scene — combine all UI into one root component
For full component props (UiEntity, Label, Button, Input, Dropdown), layout patterns, and responsive design, see {baseDir}/references/ui-components.md.
More from dcl-regenesislabs/opendcl
optimize-scene
Optimize Decentraland scene performance. Scene limit formulas (triangles, entities, materials, textures, height per parcel count), object pooling, LOD patterns, texture optimization, system throttling, and asset preloading. Use when the user wants to optimize, improve performance, fix lag, reduce load time, check limits, or reduce entity/triangle count. Do NOT use for deployment (see deploy-scene).
51game-design
Plan and design Decentraland games and interactive experiences. Scene limit formulas, performance budgets, texture requirements, asset preloading, state management patterns (module-level, component-based, state machines), object pooling, UX/UI guidelines, input design, and MVP planning. Use when the user wants game design advice, scene architecture, performance planning, or help structuring a game. Do NOT use for specific implementation (see add-interactivity, build-ui, multiplayer-sync).
30audio-video
Add sound effects, music, audio streaming, and video players to Decentraland scenes. Covers AudioSource (local files), AudioStream (streaming URLs), VideoPlayer (video surfaces), video events, and media permissions. Use when the user wants sound, music, audio, video screens, radio, or media playback. Do NOT use for 3D model animations (see animations-tweens).
30add-3d-models
Add 3D models (.glb/.gltf) to a Decentraland scene using GltfContainer. Covers loading, positioning, scaling, colliders, parenting, and browsing 5,700+ free assets from the OpenDCL catalog. Use when the user wants to add models, import GLB files, find free 3D assets, or set up model colliders. Do NOT use for materials/textures (see advanced-rendering) or model animations (see animations-tweens).
28nft-blockchain
NFT display and blockchain interaction in Decentraland. NftShape (framed NFT artwork), wallet checks (getPlayer, isGuest), signedFetch (authenticated requests), smart contract interaction (eth-connect, createEthereumProvider), and RPC calls. Use when the user wants NFTs, blockchain, wallet, smart contracts, Web3, crypto, or token gating. Do NOT use for player avatar data or emotes (see player-avatar).
27advanced-input
Advanced input handling in Decentraland. PointerLock (cursor capture state), InputModifier (freeze/restrict player movement), PrimaryPointerInfo (cursor position and world ray), WASD keyboard patterns, and action bar slots. Use when the user wants movement restriction, cursor control, FPS controls, input polling, or cutscene freezing. Do NOT use for basic click/hover events on entities (see add-interactivity).
26