NUI contract (Lua ⇄ web)
This is the single source of truth for messages between the Lua client and the NUI. Both sides must implement exactly this. The source file lives in the resource at docs/nui-contract.md; keep both in sync. Types are TypeScript-ish; every field marked ? is optional. All hud:* payloads are partial merges: send only what changed, the NUI keeps the rest.
Lua → NUI: SendNUIMessage({ action = <name>, data = <payload> })
| action | payload | notes |
|---|---|---|
hud:config | HudConfig | sent once after the NUI posts ready, and again on config change |
hud:visible | { visible: boolean, reason?: string } | hide whole HUD (pause menu, cinematic, /hud) |
hud:player | Partial<PlayerState> | ~4×/s while changing; only diffs |
hud:vehicle | Partial<VehicleState> | inVehicle=false hides the vehicle zone |
hud:weapon | Partial<WeaponState> | armed=false hides the weapon card |
hud:hotbar | { slots: HotbarSlot[], selected?: number, visible?: boolean } | only shown if HudConfig.components.hotbar |
theme | { brand?: string, strength?: 'accent'|'tiles'|'glass' } | brand empty/nil → neutral |
notify | Notification | push a toast; same id replaces an existing one |
notify:dismiss | { id: string } | |
notify:clear | {} | |
notify:key | { key: string } | a bound key (e.g. Y/N) was pressed in-game; NUI resolves it against the newest actionable toast |
prompt:show | Prompt | interaction prompt (centre); same id replaces |
prompt:hide | { id?: string } | no id → hide all |
progress:start | Progress | one at a time; NUI animates for duration ms |
progress:cancel | { id: string } |
NUI → Lua: fetch('https://np_hud/<name>', { method:'POST', body: JSON })
| name | body | response |
|---|---|---|
ready | {} | { ok: true } — Lua then sends hud:config + full state |
notifyAction | { id: string, action: string } | { ok: true } — Lua fires local event np_hud:notifyAction (id, action) and, if the toast came from the server, TriggerServerEvent('np_hud:server:notifyAction', id, action) |
progressDone | { id: string, cancelled: boolean } | { ok: true } |
Types
type Tint = 'neutral'|'red'|'orange'|'rust'|'pink'|'olive'|'lilac'|'green'|'blue'|'aqua'|'brand';
interface HudConfig {
locale: string; // 'en' | 'de' ...
units: 'kmh' | 'mph';
server: { name: string; logo?: string /* single char or URL */ };
theme: { brand?: string; strength?: 'accent'|'tiles'|'glass' };
scale: number; // 1 = designed for 1920x1080; NUI multiplies by viewport height / 1080
streamerMode: boolean; // hides server id
maxToasts: number; // default 4
components: { // which zones render
topBar: boolean; money: boolean; job: boolean; players: boolean; weather: boolean;
status: boolean; voice: boolean; street: boolean; compass: boolean; minimapFrame: boolean;
vehicle: boolean; weapon: boolean; hotbar: boolean; notifications: boolean;
};
status: Array<'health'|'armor'|'hunger'|'thirst'|'stamina'|'stress'|'oxygen'>; // order + selection
lowThreshold: number; // percent, default 20 → .is-low
minimap: { x: number; y: number; w: number; h: number }; // frame rect in px @1080p, matches native minimap placement
}
interface PlayerState {
id: number; players: number; maxPlayers: number;
health: number; armor: number; hunger: number; thirst: number; stamina: number; stress: number; oxygen: number; // 0..100
underwater: boolean; dead: boolean;
cash: number; bank: number; job: { label: string; grade?: string; icon?: string };
time: string; // "09:14" (in-game)
weather: { label: string; icon: string /* fa name, no prefix */ };
street: string; zone: string; heading: number; // degrees 0..360, 0 = north
voice: { range: 1|2|3; label: string; talking: boolean; radio?: string|null; radioTalking?: boolean; muted?: boolean };
}
interface VehicleState {
inVehicle: boolean; name: string; plate: string; kind: 'car'|'bike'|'boat'|'air'|'other';
speed: number; // already converted to config units
maxSpeed: number; // for the arc
rpm: number; // 0..1
gear: number|string; // 0 → 'R', 'N'
fuel: number; engine: number; // 0..100
seatbelt: boolean; // true = buckled
lights: boolean; cruise: boolean; locked: boolean; showBelt: boolean; // showBelt=false for bikes etc.
}
interface WeaponState { armed: boolean; name: string; icon?: string; clip: number; clipSize: number; reserve: number; }
interface HotbarSlot { slot: number; label: string; icon: string; tint?: Tint; count?: number; }
interface Notification {
id?: string; // generated by NUI if missing
type?: 'info'|'success'|'warning'|'error'; // default info
variant?: 'default'|'compact'|'announce'|'staff'|'call';
app?: string; title?: string; message?: string; // message supports **bold** only (no HTML)
icon?: string; tint?: Tint; badge?: number;
duration?: number; // ms, default 5000; 0 = sticky
progress?: number; // 0..100 → shows static bar instead of timer
meta?: Array<{ icon: string; label: string }>;
actions?: Array<{ id: string; label: string; key?: string /* 'Y' */; style?: 'default'|'primary'|'success' }>;
sound?: boolean;
}
interface Prompt { id: string; icon?: string; tint?: Tint; keys: Array<{ key: string; label: string }>; }
interface Progress { id: string; label: string; icon?: string; duration: number; tint?: Tint; cancelKey?: string; }Public Lua API
See Exports. Client exports: Notify(data | title, message?, type?, duration?) → id, DismissNotify(id), ShowPrompt(prompt), HidePrompt(id?), Progress(data) → boolean (blocking, returns true if finished), SetTheme(brand, strength), SetVisible(bool), SetHotbar(slots, selected), SetSeatbelt(bool), IsVisible(). Server exports: Notify(src, data), NotifyAll(data), Announce(data). Events: client np_hud:client:notify (data), local np_hud:notifyAction (id, action), server np_hud:server:notifyAction (id, action).
Lua notes
Clarifications from the Lua side (client/). They don't change the contract above.
- Diffing.
hud:player,hud:vehicleandhud:weapononly carry top-level keys whose value changed. Nested objects (job,weather,voice) are compared deeply and always sent complete. Afterready, Lua sendshud:config,theme, the full cached state of all three channels,hud:visible, the lasthud:hotbar, all shown prompts and any toasts queued before the NUI was ready. voice.radio. Lua sendsfalse(notnull) when the player isn't on a radio, because a Luanilfield disappears from JSON. Treat any falsy value as "no radio".heading. Compass bearing (0 = N, 90 = E, clockwise). It follows the gameplay camera by default (-GetGameplayCamRot(2).z), or the ped/vehicle (360 - GetEntityHeading) withConfig.CompassFollowsCamera = false. It's sent by the vehicle loop (100 ms) in a vehicle and by the player loop (500 ms) on foot.minimap. Lua uses the same rect as the NUI: 1080p px from the top-left, scaled by screen height / 1080 only (see Development → NUI implementation notes). The native minimap is placed to fill the whole rect, under the 5px ring.- Standalone. Without a framework,
components.moneyandcomponents.jobarefalse, andhunger,thirstandstressare removed fromstatus. Those fields are never sent. ESX withoutesx_statusdrops hunger and thirst, and ESX drops stress unless anesx_statusentry namedstressshows up. hud:visible.reasonis one ofpause,loading(character not loaded or player switch),user(/hud) orapi(SetVisible(false)). It is omitted when the HUD is visible.- Ids. Toasts created on the client get
cl:*ids and toasts from the server getsv:*ids.notifyActionfor ansv:*id is forwarded to the server, which checks it. Progress ids arepg:*when the caller doesn't supply one. notify:keyis only sent while an actionable toast may still be on screen.keyis the configured logical key (Config.NotifyKeys), even if the player rebound it.progress:start.cancelKeyis alwaysConfig.Progress.cancelKey(thehudcancelkey mapping's default) and is left out whencanCancel = false.- Hotbar. With
Config.Hotbar.autoHide > 0, Lua sendsvisible = trueon everySetHotbarandvisible = falseonce the delay has passed.