UI development
The HUD's interface is a React 18 + TypeScript app in web/, built with Vite into web/build.
This page is for developers who change that UI: how to build it, how to preview it in a browser, and the messages that pass between Lua and React.
You need this page when you edit editable/shared/js.json (it is compiled into the UI), or change anything in web/src.
Everything else — config.lua, locales — works without a build.
Building the UI
You need Node.js 18 or newer and pnpm.
cd koja-hud/web
pnpm install # once, or after updating the HUD
pnpm build # type-checks, then writes web/build
Restart the resource and reconnect — or press F8 and run ensure koja-hud — to load the new build.
| Script | What it does |
|---|---|
pnpm build | Production build into web/build |
pnpm start | Dev server with hot reload, for the browser |
pnpm start:game | Rebuilds web/build on every save, for testing in-game |
pnpm lint | ESLint |
Previewing in a browser
pnpm start opens the UI in a normal browser with sample data: status bars, the speedometer, notifications, a progress bar, the text UI, the killfeed and status effects, and the settings menu.
It is the quickest way to work on styles.
The sample data lives at the top of web/src/containers/App.tsx.
Project layout
| Path | Contents |
|---|---|
web/src/containers/App.tsx | Root: start-up sync with the game, HUD, settings, vehicle menu |
web/src/components/Hud/ | One folder per element: Status, CarHud, Notify, Progressbar, Textui, Informations, Keybinds, Compass, Weapon, Watermark, Killfeed, Warnings, StatusEffects |
web/src/components/Hud/*/styles/ | The style variants of status, speedometer and notifications |
web/src/components/Settings/ | Settings menu, layout editor, controls (switch, slider, colour, dropdown) |
web/src/components/VehicleMenu/ | Vehicle control menu |
web/src/providers/ | Settings (stored in browser storage), locale, visibility |
web/src/utils/sanitizeHtml.ts | The allowlist for HTML in notifications |
Messages from Lua to the UI
Lua sends SendNUIMessage({ action = name, data = payload }) through KOJA.Client.SendReactMessage(name, payload); React subscribes with useNuiEvent(name, handler).
| Action | Payload | Effect |
|---|---|---|
koja_hud:openHud | true | Show the HUD after the character loads |
koja_hud:refreshHud | partial HudData | Merge new values — see below |
koja_hud:showCarHud | boolean | Entered or left a vehicle |
koja_hud:toggleHud | boolean | ToggleHUD export |
koja_hud:pauseHud | boolean | Pause menu opened or closed |
koja_hud:hideComponents | { status, carhud, … } | Forced-hidden components |
koja_hud:setTalking | boolean | Voice chat activity |
koja_hud:sendNotify | notification | New notification |
koja_hud:startProgressbar | { label, icon, time, … } | Start the progress bar |
koja_hud:cancelProgressbar | — | Stop it |
koja_hud:startTextui | { input, type, desc } | Show the text UI |
koja_hud:cancelTextui | — | Hide it |
koja_hud:killfeed | { killer, victim, weapon, distance, headshot, self } | New killfeed entry |
koja_hud:killstreakReset | {} | The player died |
koja_hud:statuseffects | list of effects | Full list of active effects |
koja_hud:minimapRect | { enabled, left, bottom, width, height } | Where the minimap is, for the layout editor |
koja_hud:openSettings | true | Open the settings menu |
koja_hud:openVehicleMenu | vehicle state | Open the vehicle menu |
koja_hud:closeVehicleMenu | true | Close it |
setLocale | locale object | Texts from locales/<code>.json |
Note the casing: startTextui and cancelTextui here, while the public net events are koja_hud:startTextUI and koja_hud:cancelTextUI.
koja_hud:refreshHud
Every field is optional; whatever is sent is merged into the current state.
KOJA.Client.SendReactMessage('koja_hud:refreshHud', {
status = {
{ id = 'health', status = 87 }, -- health, shield, food, water, stamina, oxygen, voice, stress
},
informations = { cash = 1200, bank = 54000, job = 'Police', id = 12 },
car = {
speed = 23, -- metres per second; the UI converts to km/h or mph
gear = 3, -- or 'N', 'R'
engine = true,
keys = false, -- vehicle locked
fuel = { type = 'gas', min = 64, max = 100 }, -- type 'gas' or 'electric'
nitro = 80,
seatbelt = true,
rpm = 0.42, -- 0-1
engineHealth = 96,
lights = true, highbeam = false,
indicators = 0, -- GetVehicleIndicatorLights bitmask
cruise = false
},
compass = { heading = 270 },
weapon = { active = true, name = 'Pistol', ammo = 9, total = 48 },
watermark = { text = 'MY SERVER' }
})
Callbacks from the UI to Lua
React calls fetchNui(name, body), which posts to https://koja-hud/<name>; Lua answers in RegisterNUICallback.
| Callback | Body | Does |
|---|---|---|
koja_hud:getConfig | — | Returns server defaults, enabled features (compass, watermark) and the keys allowed for binds |
loadLocale | — | Lua replies with a setLocale message |
koja_hud:closeSettings | — | Releases NUI focus |
koja_hud:updateMinimap | { x, y, scale } | Moves and resizes the minimap |
koja_hud:updateMinimapPosition | { x, y } | Moves the minimap |
koja_hud:updateMinimapVisibility | { mode } | always, vehicle, foot, never |
koja_hud:updateKillfeed | { enabled, anyDistance, distance } | Player's killfeed filter |
koja_hud:updateCustomBinds | { binds = [{ key, command }] } | Player's custom binds |
koja_hud:updateBuiltinBinds | { binds = [{ id, key, command }] } | Player's keys for the HUD's own actions |
koja_hud:closeVehicleMenu | — | Releases NUI focus |
koja_hud:vehicleAction | { action, index? } | engine, seatbelt, lights, indicatorLeft, indicatorRight, hazard, door (with index 0–5); returns the new vehicle state |
On start the UI calls getConfig and then sends the player's stored binds, minimap position, minimap visibility and killfeed filter back to Lua, so player choices apply from the first frame.
Adding a setting
- Add the default under
default_settingsineditable/shared/js.json. - Add a control to a window in
options_settings, withidset to the setting's path — the menu builds itself from that list. - Add its texts to
ui.settingsin every locale file. - Read it in your component with
const { settings } = useSettings(). pnpm build.
The settings type is inferred from js.json, so TypeScript knows about the new field without further changes.