Skip to Content
ScriptingNUI (User Interface)

FiveM NUI

FiveM NUI renders HTML, CSS, and JavaScript in an in-game Chromium frame. A typical resource sends state from a client script to the browser with SendNUIMessage, controls keyboard and mouse focus with SetNuiFocus, and receives browser requests through an NUI callback.

Minimal NUI resource

Use this structure:

my-ui/ ├── fxmanifest.lua ├── client.lua └── html/ ├── index.html ├── app.js └── styles.css

Declare the page and every client-delivered asset in fxmanifest.lua:

fx_version 'cerulean' game 'gta5' ui_page 'html/index.html' files { 'html/index.html', 'html/app.js', 'html/styles.css' } client_script 'client.lua'

The cerulean FX version gives NUI pages a secure context. Use current https:// NUI callback URLs, not legacy http:// or nui:// examples.

Send data from Lua to NUI

Open the interface, give it focus, and send a JSON-encodable Lua table:

local isOpen = false local function setUiOpen(open) isOpen = open SetNuiFocus(open, open) SendNUIMessage({ type = 'setVisible', visible = open }) end RegisterCommand('example-ui', function() setUiOpen(not isOpen) end, false)

On the browser side, listen for the message event and validate the message shape before changing the DOM:

const app = document.querySelector('#app'); window.addEventListener('message', (event) => { if (event.data?.type !== 'setVisible') { return; } app.hidden = !event.data.visible; });

SendNUIMessage serializes the Lua table to JSON. Values such as functions, entity objects, and cyclic tables cannot be encoded.

Close NUI and release focus

A UI that hides visually but keeps focus will trap keyboard or mouse input. Always release focus when closing it.

Browser JavaScript:

async function closeUi() { const response = await fetch( `https://${GetParentResourceName()}/close`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=UTF-8', }, body: JSON.stringify({}), }, ); if (!response.ok) { throw new Error(`Close callback failed: ${response.status}`); } return response.json(); } window.addEventListener('keydown', (event) => { if (event.key === 'Escape') { closeUi().catch(console.error); } });

Lua client callback:

RegisterNuiCallback('close', function(_, cb) isOpen = false SetNuiFocus(false, false) SendNUIMessage({ type = 'setVisible', visible = false }) cb({ ok = true }) end)

Always call cb, including error paths. Otherwise the browser request waits until it times out.

Send structured callback data

Browser JavaScript:

async function getItem(itemId) { const response = await fetch( `https://${GetParentResourceName()}/getItem`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=UTF-8', }, body: JSON.stringify({ itemId }), }, ); const data = await response.json(); if (!response.ok || data.error) { throw new Error(data.error ?? `Request failed: ${response.status}`); } return data; }

Lua client callback:

local itemCache = { water = { label = 'Water', weight = 500 } } RegisterNuiCallback('getItem', function(data, cb) if type(data.itemId) ~= 'string' then cb({ error = 'itemId must be a string' }) return end local item = itemCache[data.itemId] if not item then cb({ error = 'Item not found' }) return end cb({ ok = true, item = item }) end)

Treat NUI input as untrusted client input. A browser callback must not grant money, items, or permissions. Send a separate server event and validate the actual game state on the server.

React and other frontend builds

FiveM does not require React. If you use a build tool:

  1. Configure relative asset paths so files resolve inside the resource.
  2. Build into a directory declared in the manifest files list.
  3. Keep source maps out of production if they expose private source.
  4. Test the production build inside FiveM, not only in a normal browser.
  5. Make the initial page work while hidden to prevent a visible flash.

An external ui_page is possible, but it introduces network availability, deployment, and content-security concerns. Bundled resource files are the simpler default.

Debug NUI

With FiveM running, open the NUI developer tools through the nui_devTools command in the F8 console when developer mode is enabled. CEF remote debugging is also available at http://localhost:13172/.

Check:

  • Browser console errors
  • The message event payload received from SendNUIMessage
  • Callback request URL, status, and response JSON
  • Missing files in fxmanifest.lua
  • Focus state after open, close, resource stop, and resource restart

Release focus if the resource stops:

AddEventHandler('onClientResourceStop', function(resourceName) if resourceName ~= GetCurrentResourceName() then return end SetNuiFocus(false, false) end)

Common NUI problems

The page is blank

  • Confirm ui_page points to the correct HTML file.
  • Add the HTML, JavaScript, CSS, fonts, and images to files.
  • Inspect the NUI console for missing relative assets.
  • Confirm the production build does not generate root-relative /assets/... URLs.

SendNUIMessage does nothing

  • Register window.addEventListener('message', ...) before opening the UI.
  • Confirm the message type matches exactly.
  • Confirm the resource containing the NUI page sent the message.
  • Check that all values are JSON-encodable.

The callback returns 404

  • Use https://${GetParentResourceName()}/callbackName.
  • Match the browser path to the registered callback name.
  • Confirm the callback is registered in a client script that has started.

The callback stalls

Call cb on every success and failure path, and return after an error response.

Keyboard or mouse remains captured

Call SetNuiFocus(false, false) when closing and when the resource stops. If you use SetNuiFocusKeepInput, explicitly disable it during the same cleanup.