Building an NUI/JS Frontend for Your FiveM Script
NUI (New UI) is how FiveM lets you build fully custom interfaces with web technology plain HTML, CSS, and JavaScript, or a full framework like React or Vue rendered inside the game using a built-in Chromium view. When you see a slick banking app, an inventory grid, or a phone in a roleplay server, that is NUI. Under the hood it is just a web page that talks to your Lua script through two channels: SendNUIMessage going from Lua to the page, and RegisterNUICallback plus fetch going from the page back to Lua.
This guide shows you the complete loop: declaring your UI in the manifest, opening and focusing it, passing data in, sending actions back, and the part beginners always miss closing it cleanly so the player isn't left with a frozen cursor. PlayDeck teaches NUI with AI: you describe the interface and the data it needs, the AI scaffolds the HTML/CSS/JS and the matching Lua message handlers, and you steer the design and behavior.
Declaring your UI in fxmanifest.lua
NUI starts in your resource manifest. You tell FiveM which HTML file is the interface with the ui_page directive, and you list every asset the page loads in the files block so the framework serves them: ui_page 'html/index.html' and files { 'html/index.html', 'html/style.css', 'html/script.js' }. If a file is not listed here, the browser inside FiveM will 404 it and your UI will render blank.
Your HTML should start hidden. A common pattern is to wrap your interface in a container with display: none in CSS, then toggle it visible only when Lua tells the page to open. Starting hidden prevents the UI from flashing on screen the moment the resource loads.
Sending data from Lua to the page
To push data into the UI, call SendNUIMessage from your client Lua with a table. FiveM serializes the table to JSON and delivers it to the page. A typical open call looks like SendNUIMessage({ action = 'open', balance = 4200 }). The convention of including an action field lets a single message handler route many different message types.
On the JavaScript side you listen for these messages on the window object: window.addEventListener('message', function(event) { const data = event.data; if (data.action === 'open') { /* show UI, fill in data.balance */ } }). Everything you sent in the Lua table arrives as properties on event.data. This is the inbound half of the loop game state flowing into your interface.
Focus: making the mouse and keyboard work
An NUI page can render on screen but still not receive clicks until you give it focus. SetNuiFocus(hasKeyboard, hasCursor) controls this. Call SetNuiFocus(true, true) when you open an interactive UI to route keyboard input to the page and show the mouse cursor. For a read-only HUD overlay that needs no interaction, do not take focus at all, so the player can keep driving and looking around.
The single most important rule in all of NUI: every SetNuiFocus(true, ...) must be paired with a SetNuiFocus(false, false) when you close the UI. Forgetting this leaves the player with a locked cursor, unable to move or interact the classic 'my mouse is stuck' bug. Always release focus on close, and also handle edge cases like the player dying or disconnecting while the UI is open.
- SetNuiFocus(true, true): interactive UI (cursor + keyboard captured)
- SetNuiFocus(false, false): always call this when closing
- No focus call: passive HUD overlays the player shouldn't click
Sending actions from the page back to Lua
When the player clicks a button in your UI, the page calls back into Lua using fetch against a special NUI URL: fetch(`https://${GetParentResourceName()}/withdraw`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 500 }) }). The path after the resource name (withdraw here) is the callback name your Lua listens for.
On the Lua side you register that callback: RegisterNUICallback('withdraw', function(data, cb) local amount = data.amount /* validate and act */ cb('ok') end). FiveM parses the POST body as JSON into data automatically, and you call cb(...) to send a response back to the awaiting fetch. Critically: a NUI callback runs on the client and the client is untrusted, so any money or inventory change must be sent on to the server with TriggerServerEvent and validated there never let the UI move money directly.
Closing the UI cleanly
Closing well is what separates a polished script from a buggy one. Listen for the Escape key in JavaScript to let players back out: document.addEventListener('keyup', function(e) { if (e.key === 'Escape') { /* hide UI */ fetch(`https://${GetParentResourceName()}/close`, { method: 'POST', body: '{}' }); } }). The page hides itself, then tells Lua to drop focus via a close callback.
In Lua, your close callback hides any remaining state and calls SetNuiFocus(false, false). For robustness, also release focus when the resource stops or the player's session ends. A reliable close path Escape closes, focus is always released, no orphaned cursor is the hallmark of a well-built NUI, and it's exactly the kind of boilerplate AI can generate correctly every time while you focus on the design.
Frequently asked questions
Why is my NUI page blank?
Usually a missing file in the manifest. Every HTML, CSS, JS, image, and font your page loads must be listed in the files block of fxmanifest.lua, and ui_page must point at the right HTML path. Open the FiveM dev tools console to see 404s.
Why is my mouse cursor stuck after closing the UI?
You opened with SetNuiFocus(true, true) but never released it. Call SetNuiFocus(false, false) in your close handler, and make sure your Escape key path and resource-stop handler also release focus.
Can I use React or Vue for NUI?
Yes. NUI renders any web app, so React, Vue, or Svelte all work you build the app, output a static bundle, and point ui_page at the built index.html. For simple menus, ox_lib's built-in menus are often faster than building custom NUI.
Is it safe to handle money in a NUI callback?
No. NUI runs on the client, which players can manipulate. Treat the callback only as a request: forward it to the server with TriggerServerEvent and validate the player, balance, and permissions server-side before changing anything.