Skip to content

UI (React)

The @yagejs/ui-react package provides a React reconciler over the UI system, letting you write game UI with JSX. It includes hooks for accessing engine services and reactive stores for bridging ECS state into React.

Install both @yagejs/ui and @yagejs/ui-react, plus React:

Terminal window
npm install @yagejs/ui @yagejs/ui-react react

Both UIPlugin and UIReactPlugin must be registered before using React UI. UIReactPlugin registers the LateUpdate-phase layout pass that positions UIRoot trees after any Update-phase Transform writers (e.g. ScreenFollow):

import { UIPlugin } from "@yagejs/ui";
import { UIReactPlugin } from "@yagejs/ui-react";
engine.use(new UIPlugin());
engine.use(new UIReactPlugin());

UIRoot is a component that hosts a React tree on an entity:

import { UIRoot } from "@yagejs/ui-react";
import { Anchor } from "@yagejs/ui";
const ui = scene.spawn("ui");
ui.add(new Transform());
const root = new UIRoot({ anchor: Anchor.Center });
ui.add(root);
root.render(<MyMenu />);

By default the React tree renders into the auto-provisioned screen-space "ui" layer, positioned by anchor and optional offset against the viewport. Pass layer: "<name>" to mount on any layer declared on Scene.layers.

For UI that tracks a specific game entity (nameplates, health bars, damage numbers), set positioning: "transform" on the UIRoot and pair with ScreenFollow from @yagejs/renderer. ScreenFollow writes camera.worldToScreen(target) + offset to this entity’s Transform each frame (the offset is in screen pixels, applied after projection), and the UIRoot reads that Transform instead of the viewport — so the UI tracks the target while staying axis-aligned and constant-size under any camera zoom or rotation.

import { ScreenFollow } from "@yagejs/renderer";
import { UIRoot, Panel, Text } from "@yagejs/ui-react";
import { Anchor } from "@yagejs/ui";
class EnemyNameplate extends Entity {
setup(params: {
target: Entity; camera: CameraEntity; label: string;
}) {
this.add(new Transform());
this.add(new ScreenFollow({
target: params.target,
camera: params.camera,
offset: new Vec2(0, -40), // 40 screen px above the target
}));
const root = this.add(new UIRoot({
positioning: "transform", // read Transform.worldPosition
anchor: Anchor.BottomCenter, // pivot on the rendered tree
}));
root.render(<NameplateView label={params.label} />);
}
}
function NameplateView({ label }: { label: string }) {
return (
<Panel padding={4} bg={{ color: 0x000000, alpha: 0.6, radius: 4 }}>
<Text style={{ fontSize: 11, fill: 0xffffff }}>{label}</Text>
</Panel>
);
}

UIRoot with positioning: "transform" requires a Transform on the entity and throws at add time otherwise. See the imperative UI guide for the full positioning model (the two modes, the world-space-layer variant for genuinely diegetic UI, and when to pick which).

React components mirror the imperative @yagejs/ui API:

import { Panel, Text, Button, Image, ProgressBar, Checkbox } from "@yagejs/ui-react";
function HUD() {
const [score, setScore] = useState(0);
return (
<Panel direction="column" gap={8} padding={16} bg={{ color: 0x000000, alpha: 0.7 }}>
<Text style={{ fontSize: 24, fill: 0xffffff }}>Score: {score}</Text>
<Button
width={150}
height={40}
bg={{ color: 0x4444aa }}
hoverBg={{ color: 0x5555cc }}
textStyle={{ fontSize: 16, fill: 0xffffff }}
onClick={() => setScore(s => s + 1)}
>
Add Point
</Button>
<ProgressBar
width={200}
height={16}
value={score / 10}
fillBackground={{ color: 0x44cc44 }}
trackBackground={{ color: 0x333333 }}
/>
<Checkbox
label="Mute"
checked={false}
onChange={(v) => console.log("mute:", v)}
/>
</Panel>
);
}
ComponentDescription
<Panel>Flexbox container with direction, gap, padding, bg
<Text>Text label with style
<Button>Clickable button with onClick, bg, hoverBg, pressBg
<Image>Texture display with texture, tint, alpha
<NineSlice>Nine-slice scalable texture
<ProgressBar>Progress bar (value 0–1)
<Checkbox>Checkbox with checked, onChange, label

Wrappers for advanced @pixi/ui widgets:

import {
PixiFancyButton,
PixiSlider,
PixiInput,
PixiSelect,
PixiRadioGroup,
PixiScrollBox,
} from "@yagejs/ui-react";

Access the engine context or current scene from any React component:

import { useEngine, useScene } from "@yagejs/ui-react";
function PauseButton() {
const engine = useEngine();
return (
<Button onClick={() => engine.scenes.push(new PauseScene())} width={100} height={40}>
Pause
</Button>
);
}

Bridge ECS state into React with reactive stores:

import { createStore } from "@yagejs/ui-react";
import { useStore } from "@yagejs/ui-react";
// Create a store (typically at scene level)
const gameStore = createStore({ score: 0, health: 100 });
// Write from ECS (systems, components, event handlers)
gameStore.set({ score: gameStore.get().score + 10 });
// Read from React (auto-rerenders on change)
function ScoreDisplay() {
const score = useStore(gameStore, (s) => s.score);
return <Text style={{ fontSize: 32 }}>{`Score: ${score}`}</Text>;
}

The optional selector and equality function prevent unnecessary re-renders:

// Only re-render when score changes
const score = useStore(store, (s) => s.score);
// Custom equality check
const pos = useStore(store, (s) => s.position, (a, b) => a.x === b.x && a.y === b.y);

Query ECS entities directly from React:

import { useQuery } from "@yagejs/ui-react";
function EnemyCount() {
const count = useQuery(
[EnemyTag],
(result) => result.size,
);
return <Text>Enemies: {count}</Text>;
}

The query re-evaluates each frame and only triggers a re-render when the selector result changes.

Read arbitrary scene state:

import { useSceneSelector } from "@yagejs/ui-react";
function EntityCounter() {
const count = useSceneSelector((scene) => scene.getEntities().length);
return <Text>Entities: {count}</Text>;
}
Imperative (@yagejs/ui)React (@yagejs/ui-react)
Best forSimple HUDs, static menusComplex interactive menus, forms
StateManual .setText() callsDeclarative with useState / useStore
LayoutBuilder APIJSX composition
Bundle sizeSmaller (no React)Adds React dependency
Learning curveLower if no React experienceLower if familiar with React

Use the imperative API for simple, mostly-static UI (score counters, health bars). Use React when your UI has complex state, conditional rendering, or many interactive elements.