CReact guide#
CReact is the in-game UI renderer. You write React components in TSX. At runtime they become GPU primitives (box, text, image, ...) in the QuickJS host.
New to mod panels? Start with Build a CReact mod. See User Interfaces with CReact and Courage.UI for wiring and API reference.
React basics#
CReact uses real React (components, props, hooks, JSX). If you are new to React, start with the official docs:
In mod panels, game logic and saved values belong in Luau, not just React useState. The panel reads Luau data from state and sends actions with emit. See Luau and panel interop.
useState is fine for local UI state (hover, uncommitted tab selection).
How CReact differs from the web#
| Web React | CReact |
|---|---|
DOM elements (div, span, ...) | GPU primitives (box, text, ...) |
| CSS in files or Tailwind | Mostly inline style={{ ... }} in mod panels |
fetch, window, browser APIs | Not available in the game bundle |
| Layout with Grid | Flexbox only |
At build time, the CReact JSX shim rewrites common HTML tags to primitives (div becomes box, span and p become text, button becomes box with role="button", img becomes image). Either style works in TSX.
Primitives#
| Tag | Purpose |
|---|---|
box | Container, backgrounds, borders, flex layout, buttons (role="button") |
text | Labels, paragraphs, headings (rewritten from span, p, h1–h6, ...) |
image | Textures from logical asset keys (asset / src). See Courage.Assets. |
nineslice | Resizable bordered panels from a 9-slice image |
sprite | Sub-rectangle of a texture (uv) |
textinput | Single-line text entry |
Children go inside box the same way you would nest div elements.
Image assets in mod panels#
Pass a logical asset key to image (from Courage.Assets or Courage.Layout capture):
- Static files: put PNG/JPEG/BMP under
mods/<id>/assets/and pushAssets("path.png")throughUI.setState. - Rasterized BLO:
Layout.capture("ui/screen.png")registers pixels in memory. Use the returned key instate.
Layout#
CReact lays out with flexbox. Typical panel structure:
Common style keys:
| Key | Notes |
|---|---|
flexDirection | "column" or "row" |
gap, rowGap, columnGap | Space between children |
alignItems, justifyContent | "start", "center", "end", "stretch", "space-between" |
flex | Grow/shrink on a child |
padding, paddingX, paddingY | Insets |
margin, marginX, marginY | Outer spacing |
width, height, alignSelf | Sizing |
CSS Grid (display: grid, grid-column, ...) is not supported. Use flex rows and columns instead.
Styling#
Mod panels (creact/panel.tsx) should use inline style={{ ... }} objects. The mod compiler does not run the full game UI Tailwind pipeline, and .css imports are stripped.
Supported inline examples:
Colors are hex strings or { r, g, b, a? } objects. The main menu UI (built with Vite + vite-plugin-creact) can also use className and generated style tables, but mod bundles do not include that table today.
Input and events#
Pointer and keyboard handlers attach to primitives the same way as in React:
| Handler | When it fires |
|---|---|
onClick | Primary click on the element (bubbles from children) |
onPointerDown / onPointerUp | Press and release |
onPointerEnter / onPointerLeave | Hover |
onFocus / onBlur | Focus changes |
onKeyDown / onKeyUp | Keyboard |
Make clickable areas explicit:
Clicks on a child text bubble up to a parent box that owns onClick, so you usually only need the handler on the outer box.
Event handler functions are kept in JavaScript. They are never serialized to the C++ engine.
Mod panel props#
When the game loads your panel, the host passes:
| Prop | Type | Role |
|---|---|---|
modId | string | Mod folder name |
state | Record<string, unknown> | JSON-safe data pushed from Luau via UI.setState |
emit | (action, data?) => void | Sends <modId>:<action> to Petrichor.Events |
Import ModUiPanelProps from Courage.ModUi (provided by the mod UI compiler shims).
What to avoid in mod TSX#
| Avoid | Why |
|---|---|
import.meta.env | Not available in the QuickJS runtime |
| Tailwind / CSS modules | Not wired in the mod compiler |
Calling fetch or Node APIs | Bundle targets the in-game engine |
Storing game state only in useState | Luau cannot read it. Use emit and UI.setState |
| CSS Grid | Use flex layout |
Debugging tips#
- If the panel is blank, check the log for
[mod-ui]lines (missingpanel.bundle.jsorpopup.bundle.js,evalScriptfailure, unknownmodId). - If clicks do nothing, confirm the handler is on a
boxwithrole="button"(or a bubbling parent), and that Luau registeredEvents.on("<modId>:<action>", ...). - If the counter or labels never change, confirm Luau calls
UI.setStateafter handling the event. - Rebuild the bundle after TSX edits:
cd pc/ui && bun run compile-mod-ui:one /path/to/mod.
See also#
- User Interfaces with CReact, folder layout, build commands, full example
- Courage.UI,
show,hide,showPopup,hidePopup,setState,getState - Working with events,
Petrichor.Eventsand panelemit