Skip to Content
ScriptingAdvanced NUI
Report an error
Open in ChatGPTOpen in Claude

Advanced FiveM NUI

Choose the surface before writing the UI: use ui_page for an interactive overlay, DUI for a browser rendered to a texture, and loadscreen for the connection screen. They have different input and lifecycle responsibilities. This guide extends NUI messages and callbacks with reproducible examples rather than assumed prop texture names.

DUI: browser output on a game texture

This self-contained client resource displays a small browser texture on screen using DrawSprite. It does not modify a game prop or require a particular asset. Create resources/[local]/doc_dui/ with these files.

Create the DUI browser

fxmanifest.lua:

fx_version 'cerulean' game 'gta5' client_script 'client.lua' files { 'html/screen.html' }

html/screen.html:

<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Docs DUI demonstration</title> <style> body { margin: 0; padding: 24px; background: #18202b; color: white; font: 24px sans-serif; } </style> </head> <body><h1>DUI is ready</h1><p>This page is rendered into a runtime texture.</p></body> </html>

Turn the handle into a runtime texture

client.lua includes creation, bounded readiness waiting, drawing, and resource-stop cleanup:

local dui local running = true local function destroyBrowser() if dui then DestroyDui(dui) dui = nil end end AddEventHandler('onResourceStop', function(resourceName) if resourceName ~= GetCurrentResourceName() then return end running = false destroyBrowser() end) CreateThread(function() local url = ('https://cfx-nui-%s/html/screen.html'):format(GetCurrentResourceName()) dui = CreateDui(url, 512, 256) if not dui or dui == 0 then dui = nil print('[doc_dui] Could not create the browser.') return end local started = GetGameTimer() while running and dui and not IsDuiAvailable(dui) do if GetGameTimer() - started > 5000 then print('[doc_dui] Browser readiness timed out; check the resource URL and files.') destroyBrowser() return end Wait(0) end if not running or not dui then return end local dictionary = CreateRuntimeTxd('doc_dui_txd') CreateRuntimeTextureFromDuiHandle(dictionary, 'panel', GetDuiHandle(dui)) while running do DrawSprite('doc_dui_txd', 'panel', 0.5, 0.3, 0.4, 0.2, 0.0, 255, 255, 255, 255) Wait(0) -- Drawing is frame-dependent; browser creation is not. end end)

Run refresh and ensure doc_dui in the server console while a client is connected. The client should see the panel. stop doc_dui should remove it and destroy its browser. Test repeated restarts rather than leaving old browser instances alive during development.

DUI availability does not prove the HTML loaded successfully. For a blank panel, inspect the actual resource name, declared file, and NUI developer-tools network errors before changing rendering code.

Apply the texture to a prop

A prop integration requires the actual texture dictionary and texture name from that asset. Do not guess them from the prop model name. AddReplaceTexture replaces a named texture, potentially affecting multiple instances that use it. A render-target or model-specific integration needs its own asset inspection and cleanup, including removal of any replacement before destroying its DUI.

Keep the on-screen example working first; it separates a browser-loading failure from an asset-binding failure. See assets and streaming for the asset side.

DUI is not directly interactive

Creating the browser does not automatically map the player’s mouse to the surface. DUI has mouse-move, mouse-down, mouse-up, and wheel natives. A world-space screen must translate the interaction position into browser-pixel coordinates and forward matching press/release events. Use an ordinary NUI overlay when you need a form with keyboard input and accessible focus behavior.

The official DUI manual  links the individual native contracts and rendering options.

Choosing the right focus mode

Passive HUDs should not take keyboard or cursor focus. Open an interactive overlay deliberately and release it in both its close path and its resource-stop cleanup. For an existing NUI resource, this Lua extension demonstrates that lifecycle:

local panelOpen = false local function closePanel() if panelOpen then SetNuiFocus(false, false) end panelOpen = false SendNUIMessage({ type = 'close' }) end RegisterCommand('docpanel', function() if panelOpen then closePanel(); return end panelOpen = true SetNuiFocus(true, true) SendNUIMessage({ type = 'open' }) end, false) RegisterNUICallback('close', function(_, reply) closePanel() reply({ ok = true }) -- Complete every callback; do not leave fetch pending. end) AddEventHandler('onResourceStop', function(resourceName) if resourceName == GetCurrentResourceName() then closePanel() end end)

Pair the open and close messages with your page’s visibility handler from NUI basics. A resource-local flag does not arbitrate focus between multiple menus; coordinate with your framework’s UI owner when several resources can open overlays.

The ESC-to-close pattern

In the existing NUI page’s JavaScript, request the same close path from Escape:

window.addEventListener('keydown', async (event) => { if (event.key !== 'Escape') return; try { 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 failed: ${response.status}`); } catch (error) { console.error('Could not close the NUI panel', error); } });

Test opening, Escape, command-toggle closing, and restarting the resource while the overlay is focused. Do not rely only on a browser key event to restore gameplay input.

Customizing the loading screen

Create a separate doc_loadscreen resource. Its fxmanifest.lua is:

fx_version 'cerulean' game 'gta5' loadscreen 'html/loadscreen.html' files { 'html/loadscreen.html', 'html/progress.js' }

html/loadscreen.html:

<!doctype html> <html lang="en"> <head><meta charset="utf-8"><title>Connecting to the server</title></head> <body> <h1>Connecting to the server</h1> <label for="connection-progress">Loading game data</label> <progress id="connection-progress" value="0" max="1"></progress> <p id="connection-status">Waiting for progress...</p> <script src="progress.js" defer></script> </body> </html>

html/progress.js handles the documented payload and rejects malformed values:

const progress = document.getElementById('connection-progress'); const status = document.getElementById('connection-status'); window.addEventListener('message', ({ data }) => { if (!data || data.eventName !== 'loadProgress') return; if (typeof data.loadFraction !== 'number' || !Number.isFinite(data.loadFraction)) return; const fraction = Math.min(1, Math.max(0, data.loadFraction)); progress.value = fraction; status.textContent = `Loading game data: ${Math.round(fraction * 100)}%`; });

Add ensure doc_loadscreen on your development server and reconnect. Avoid enabling multiple resources that compete to supply a loading screen. A local browser preview can exercise the UI with window.postMessage({ eventName: 'loadProgress', loadFraction: 0.5 }, '*'); this is a UI test, not proof of an in-game connection.

Use real progress messages rather than a fake timer. A full bar describes this loading progress, not necessarily completion of your character-selection or spawning workflow. Use textContent, not innerHTML, for any player-provided handover text.

Manual shutdown is optional

The default example does not opt into manual lifetime management. Add loadscreen_manual_shutdown 'yes' only when your actual client spawn flow will call ShutdownLoadingScreenNui() when ready. A plain ShutdownLoadingScreen() call is not the same as dismissing a manually managed loading-screen NUI. Do not add an arbitrary timer to hide an unresolved connection or spawn failure.

The official loading-screen guide  defines loadProgress, loadFraction, handover data, and the shutdown behavior.

Continue with NUI basics, key bindings, resource lifecycle, and profiling UI-related script work.