Build a CReact mod#
Build a mod with a CReact overlay. The example is a counter: the panel calls emit, Luau handles the event and pushes updates with UI.setState. TSX lives under creact/. Compile to panel.bundle.js (menu panel) or popup.bundle.js (in-game overlay) with Bun before you test or ship.
For a real-game panel with Adopt / Not now buttons, see User Interfaces with CReact (Cat Companion mod).
See the CReact guide for primitives and layout. See User Interfaces with CReact and Courage.UI for more patterns and API detail.
Prerequisites#
- The game source tree (you need
pc/ui/to run the mod panel compiler). - Bun. The UI toolchain and mod compiler scripts use Bun, not Node.
Install Bun#
Follow the official guide: Bun installation.
Quick check after install:
On macOS and Linux the installer usually puts bun in ~/.bun/bin. Add that directory to your PATH if the command is not found.
CMake can also find Bun when you build the game with -DCR_BUILD_REACT_UI=ON (default).
One-time UI dependencies#
From the repo root:
You only need to repeat this when pc/ui/package.json or the lockfile changes.
Create the mod folder#
Under your mods directory (for example pc/build/mods/ during local dev):
mods/
└── hello-ui/
├── mod.json
├── main.luau # Luau entry (or src/main.ts compiled to out/main.luau)
└── creact/
└── panel.tsx
The folder name (hello-ui) must match the mod id and the string you pass to UI.show.
Write mod.json#
Luau entry:
{
"id": "hello-ui",
"name": "Hello UI",
"version": "1.0.0",
"hostVersion": "0.8.0",
"engineVersion": "1.0.0",
"type": "luau",
"entry": "main.luau"
}
TypeScript entry (after ts-luau compile):
{
"id": "hello-ui",
"name": "Hello UI",
"version": "1.0.0",
"hostVersion": "0.8.0",
"engineVersion": "1.0.0",
"type": "luau",
"entry": "out/main.luau"
}
Write creact/panel.tsx#
The host passes state from Luau and emit back to Luau. Use inline style objects in mod panels.
import type { ModUiPanelProps } from "Courage.ModUi";
export const title = "Hello from my mod";
export default function Panel({ state, emit }: ModUiPanelProps) {
const count = (state.count as number) ?? 0;
return (
<box style={{ gap: 12, flexDirection: "column", padding: 16 }}>
<text style={{ fontSize: 16, color: "#cbd5e1" }}>
Count is owned by Luau via UI.setState.
</text>
<box
role="button"
style={{
padding: 10,
backgroundColor: "#2563eb",
borderRadius: 8,
alignSelf: "flex-start",
}}
onClick={() => emit("increment")}
>
<text style={{ color: "#fff", fontSize: 14 }}>
Clicked {count} times
</text>
</box>
</box>
);
}
Write the entry script#
Luau owns the counter. Clicks show up on Petrichor.Events as hello-ui:increment. Use main.luau, or compile src/main.ts to out/main.luau (TypeScript scripting guide).
local Events = require("Petrichor.Events")
local Log = require("Petrichor.Log")
local UI = require("Courage.UI")
local HelloUi = mod("hello-ui")
local MOD_ID = "hello-ui"
function HelloUi:init()
UI.setState(MOD_ID, { count = 0 })
Events.on(MOD_ID .. ":increment", function()
local state = UI.getState(MOD_ID)
state.count = (state.count or 0) + 1
UI.setState(MOD_ID, state)
Log.info(MOD_ID, "count is now " .. tostring(state.count))
end)
UI.show(MOD_ID)
end
function HelloUi:shutdown()
UI.hide()
end
return HelloUi
import * as Events from "Petrichor.Events"
import * as Log from "Petrichor.Log"
import * as UI from "Courage.UI"
declare function mod(name: string): any
const HelloUi = mod("hello-ui")
const MOD_ID = "hello-ui"
HelloUi.init = function () {
UI.setState(MOD_ID, { count: 0 })
Events.on(`${MOD_ID}:increment`, () => {
const state = UI.getState(MOD_ID) as { count?: number }
const count = (state.count ?? 0) + 1
UI.setState(MOD_ID, { count })
Log.info(MOD_ID, `count is now ${count}`)
})
UI.show(MOD_ID)
}
HelloUi.shutdown = function () {
UI.hide()
}
export = HelloUi
Compile the panel#
The game does not compile TSX at runtime. Build panel.bundle.js before you test or ship.
Compile this mod only:
You should get mods/hello-ui/creact/panel.bundle.js next to panel.tsx.
To compile every mod under mods/ that has a creact/ entry:
A full game build with CR_BUILD_REACT_UI=ON runs the same compile step automatically for mods in the repo mods/ tree.
Run the game#
- Confirm
creact/panel.bundle.jsexists in the mod folder. - Enable the mod in the in-game mod list (or place it in the scanned
mods/directory). - Start the game.
UI.show("hello-ui")opens the overlay when the mod loads.
If the panel is blank, check the log for [mod-ui] messages (missing bundle, wrong modId, or compile error).
Day-to-day workflow#
| Step | Command / action |
|---|---|
| Edit layout or handlers | Change creact/panel.tsx |
| Rebuild bundle | cd pc/ui && bun run compile-mod-ui:one /path/to/mods/hello-ui |
| Edit game logic | Change main.luau, or src/main.ts then re-run ts-luau |
| Ship the mod | Include panel.bundle.js in the folder you distribute |
Ship panel.bundle.js. You can omit panel.tsx from a release build if you only distribute the bundle.
See also#
- CReact guide, primitives, layout, styling
- User Interfaces with CReact, patterns and full reference example
- Courage.UI,
show,showPopup,hide,hidePopup,setState,getState - Working with events,
Petrichor.Eventsand panelemit