NUI basics: HTML interfaces in FiveM
Build FiveM UIs with NUI: ui_page and files, SendNUIMessage, RegisterNUICallback with fetch, SetNuiFocus, dev tools, React or Vue with a build step, and common pitfalls.
Por enquanto, esta documentação está em inglês.
NUI is FiveM’s built in Chromium (CEF) browser. Every resource can have a web page that sits on top of the game: HUDs, menus, phones, inventories, loading screens. You write it with HTML, CSS and JavaScript, or any framework that builds to static files (React, Vue, Svelte, Solid…).
How it fits together
Lua client script ── SendNUIMessage({...}) ──► your page (window 'message' event)
Lua client script ◄── RegisterNUICallback ──── fetch('https://<resource>/<name>')- Lua sends data to the page with
SendNUIMessage. - The page sends data back with an HTTP style
fetchtohttps://<resource-name>/<callback-name>, which triggers aRegisterNUICallbackhandler in Lua. SetNuiFocusdecides whether the page gets the mouse and keyboard.- The page never talks to the server directly. If the server needs to know, Lua forwards it with an event.
A minimal example
nui_demo/
├─ fxmanifest.lua
├─ client.lua
└─ html/
├─ index.html
├─ style.css
└─ app.jsfx_version 'cerulean'
game 'gta5'
client_script 'client.lua'
ui_page 'html/index.html'
files {
'html/index.html',
'html/style.css',
'html/app.js',
}Every file the page loads must be listed in files, or the client never downloads it and you get a blank or broken UI.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="panel" class="hidden">
<h1 id="title">Hello</h1>
<button id="close">Close</button>
</div>
<script src="app.js"></script>
</body>
</html>html, body { margin: 0; background: transparent; font-family: sans-serif; }
.hidden { display: none; }
#panel {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
padding: 24px; background: rgba(15, 15, 15, 0.9); color: #fff; border-radius: 8px;
}Keep the body background transparent, or the page covers the game.
const panel = document.getElementById('panel');
const title = document.getElementById('title');
window.addEventListener('message', (event) => {
const data = event.data;
if (data.action === 'open') {
title.textContent = data.title;
panel.classList.remove('hidden');
} else if (data.action === 'close') {
panel.classList.add('hidden');
}
});
function post(name, body = {}) {
return fetch(`https://${GetParentResourceName()}/${name}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
body: JSON.stringify(body),
}).then((r) => r.json());
}
document.getElementById('close').addEventListener('click', () => post('close'));
document.addEventListener('keyup', (e) => {
if (e.key === 'Escape') post('close');
});GetParentResourceName() is available inside NUI pages and returns your resource name, so the callback URL stays right if someone renames the folder.
local open = false
local function setOpen(state)
open = state
SetNuiFocus(state, state) -- keyboard focus, mouse cursor
SendNUIMessage({ action = state and 'open' or 'close', title = 'Hello from Lua' })
end
RegisterCommand('demo', function()
setOpen(not open)
end, false)
RegisterNUICallback('close', function(data, cb)
setOpen(false)
cb({ ok = true }) -- always answer, or fetch() hangs
end)ensure nui_demo, type /demo, and the panel appears with a mouse cursor. Esc or the button closes it.
Focus
SetNuiFocus(hasFocus, hasCursor):
SetNuiFocus(true, true): the page gets keyboard and mouse. The player can’t move or use game controls. Use for menus.SetNuiFocus(false, false): the page is display only (HUDs). The game keeps control.SetNuiFocusKeepInput(true)lets the game still receive keyboard input while the page has focus, for UIs where the player keeps walking. Disable the controls you don’t want yourself.
Warning
Always give the player a way out (Esc, a close button) and release focus on close. A UI that keeps focus traps the player, who has to quit the game. Also release focus on onResourceStop.
If your UI opens with Tab (inventories do), check IsNuiFocused() first so it doesn’t steal focus from another open UI like the txAdmin menu.
Debugging NUI
Type nui_devtools in the F8 console to open Chrome DevTools for NUI: console logs, element inspector, network tab. Errors in your JavaScript show there, not in F8.
Other tips:
- Develop the page in a normal browser first, faking the
messageevents withwindow.postMessage({ action: 'open', title: 'test' }). ui_pagecan point to a dev server URL (ui_page 'http://localhost:5173') while you work, so hot reload works. Switch back to the built file before you ship.
React, Vue and friends
Frameworks work fine. You build to static files and point ui_page at the built index.html:
ui_page 'web/build/index.html'
files {
'web/build/index.html',
'web/build/**/*',
}- Use relative asset paths in your build (Vite:
base: './'), because the page is served fromhttps://cfx-nui-<resource>/. - Starters save setup time: project-error/fivem-react-boilerplate-lua (React + TypeScript + Lua) and overextended/fivem-typescript-boilerplate.
- Ship the built folder in releases. Server owners shouldn’t need Node to run your resource.
Loading screens
A loading screen is a special NUI page shown while the player joins:
loadscreen 'html/index.html'
files { 'html/**/*' }
-- optional: keep it until you close it yourself
loadscreen_manual_shutdown 'yes'With manual shutdown, call ShutdownLoadingScreenNui() from a client script when your spawn is ready, or players stay on the loading screen forever.
Performance
- NUI pages that animate constantly (CSS animations, requestAnimationFrame, big videos) cost GPU and CPU on every client. Pause them when hidden.
- Don’t
SendNUIMessageevery frame. Send when data changes, or a few times per second for HUD values. - Keep images reasonable in size. Every file in
filesis downloaded by every player.
Common mistakes
| Symptom | Cause |
|---|---|
| Blank UI | ui_page or its assets not listed in files, wrong path, or absolute asset paths in a framework build. |
fetch never resolves |
The Lua RegisterNUICallback handler didn’t call cb(). |
| UI shows but clicks do nothing | SetNuiFocus(true, true) not called. |
| Player stuck with a cursor | Focus not released on close or on resource stop. |
| White or black background over the game | body or html background isn’t transparent. |
| Works in browser, not in game | Using APIs NUI doesn’t allow, or the callback URL doesn’t match the resource name. Use GetParentResourceName(). |
Next: Exports and dependencies.
