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.cssDeclare 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:
- Configure relative asset paths so files resolve inside the resource.
- Build into a directory declared in the manifest
fileslist. - Keep source maps out of production if they expose private source.
- Test the production build inside FiveM, not only in a normal browser.
- 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
messageevent payload received fromSendNUIMessage - 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_pagepoints 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
typematches 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.