Skip to content

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> }) ​

actionpayloadnotes
hud:configHudConfigsent 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:playerPartial<PlayerState>~4×/s while changing; only diffs
hud:vehiclePartial<VehicleState>inVehicle=false hides the vehicle zone
hud:weaponPartial<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
notifyNotificationpush 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:showPromptinteraction prompt (centre); same id replaces
prompt:hide{ id?: string }no id → hide all
progress:startProgressone at a time; NUI animates for duration ms
progress:cancel{ id: string }

NUI → Lua: fetch('https://np_hud/<name>', { method:'POST', body: JSON }) ​

namebodyresponse
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 ​

ts
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:vehicle and hud:weapon only carry top-level keys whose value changed. Nested objects (job, weather, voice) are compared deeply and always sent complete. After ready, Lua sends hud:config, theme, the full cached state of all three channels, hud:visible, the last hud:hotbar, all shown prompts and any toasts queued before the NUI was ready.
  • voice.radio. Lua sends false (not null) when the player isn't on a radio, because a Lua nil field 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) with Config.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.money and components.job are false, and hunger, thirst and stress are removed from status. Those fields are never sent. ESX without esx_status drops hunger and thirst, and ESX drops stress unless an esx_status entry named stress shows up.
  • hud:visible.reason is one of pause, loading (character not loaded or player switch), user (/hud) or api (SetVisible(false)). It is omitted when the HUD is visible.
  • Ids. Toasts created on the client get cl:* ids and toasts from the server get sv:* ids. notifyAction for an sv:* id is forwarded to the server, which checks it. Progress ids are pg:* when the caller doesn't supply one.
  • notify:key is only sent while an actionable toast may still be on screen. key is the configured logical key (Config.NotifyKeys), even if the player rebound it.
  • progress:start.cancelKey is always Config.Progress.cancelKey (the hudcancel key mapping's default) and is left out when canCancel = false.
  • Hotbar. With Config.Hotbar.autoHide > 0, Lua sends visible = true on every SetHotbar and visible = false once the delay has passed.

np_* FiveM resources