Advanced FiveM NUI
This page assumes familiarity with the basics in NUI and covers patterns beyond a fullscreen overlay: rendering a browser onto a game texture with DUI, choosing the right focus mode for the moment, and hooking FiveM’s loading screen.
DUI: browser output on a game texture
A normal NUI page (ui_page in fxmanifest.lua) always renders as a
fullscreen overlay. DUI (Direct User Interface) renders a browser instance
into an off-screen buffer that you apply as a texture on a prop, object, or
vehicle screen — an in-game laptop, phone, tablet, or billboard rendered by
the game’s 3D pipeline instead of drawn over the whole screen.
Create the DUI browser
local duiObject = CreateDui('https://example-resource/html/screen.html', 512, 512)
local duiHandle = GetDuiHandle(duiObject)CreateDui takes a URL (an nui:// resource path or a full HTTPS URL) and a
pixel width/height for the render target. GetDuiHandle returns the runtime
texture handle string used to wire the browser output into a texture.
Turn the handle into a runtime texture
local txd = CreateRuntimeTxd('laptop_screen')
local runtimeTexture = CreateRuntimeTextureFromDuiHandle(txd, 'screen', duiHandle)CreateRuntimeTxd creates a texture dictionary your resource owns.
CreateRuntimeTextureFromDuiHandle wraps the DUI handle as a named texture
inside that dictionary.
Apply the texture to a prop
The common technique is to swap an existing texture on a known prop model
with AddReplaceTexture, targeting the texture dictionary and texture name
the prop model already uses:
AddReplaceTexture('prop_laptop_01a', 'screen_generic_screen', 'laptop_screen', 'screen')This replaces the prop’s original screen texture globally with your DUI output for as long as the replacement is active. Every instance of that prop model in the world picks up the new texture, so this pattern fits props you control the placement of — a laptop at a specific location, a custom billboard, a tablet held by a ped.
Update and clean up
SendDuiMessage(duiObject, json.encode({ type = 'setBalance', balance = 4200 }))SendDuiMessage works like SendNUIMessage, delivering a JSON payload to the
DUI page’s message event listener. When the prop or feature is no longer
needed, destroy the DUI object to stop rendering and free the browser
instance:
DestroyDui(duiObject)An un-destroyed DUI keeps a browser instance alive and rendering every frame. Track every DUI object you create and destroy it on cleanup and resource stop.
DUI is not directly interactive
DUI has no built-in mouse/keyboard routing — the browser renders, but nothing
sends it clicks by default. If the prop’s screen needs to respond to input,
route interaction manually: detect the player interacting with the
prop (a key press near it, a target/interaction prompt) and send that as a
SendDuiMessage event, or fall back to a full NUI overlay if the surface
needs to receive text input or drag interaction.
Choosing the right focus mode
SetNuiFocus(hasFocus, hasCursor) controls whether NUI receives keyboard and
mouse input at all, and full focus is not always the right choice.
SetNuiFocus(true, true) -- keyboard + mouse captured, cursor shown
SetNuiFocus(true, false) -- keyboard captured, no cursor (rare)
SetNuiFocus(false, false) -- game keeps all inputWhen not to grab full focus
Taking SetNuiFocus(true, true) during a gameplay-critical moment — mid
combat, mid vehicle chase, during another resource’s own input capture — steals
control from the player at the worst time. Prefer:
- Partial or no focus for passive UI. A HUD element, notification, or
progress bar that only displays information doesn’t need any focus call.
Send it with
SendNUIMessageand letpointer-events: nonein CSS keep it from intercepting clicks. - Focus only while the interactive UI is actually open. Grant focus when the menu opens and release it immediately when it closes — don’t hold focus “just in case” for a UI the player might open later.
- Checking for conflicting UI before opening. If your resource opens a
focused menu, check whether another known UI state (chat input, another
resource’s menu, the pause menu) is already active before calling
SetNuiFocus, or you’ll fight another resource for input.
The ESC-to-close pattern
Handle Escape in the browser, not by polling a Lua key check, since the
key event only needs to reach the page that currently owns focus:
window.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') {
return;
}
fetch(`https://${GetParentResourceName()}/close`, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
body: JSON.stringify({}),
});
});Pair this with the callback pattern from the NUI basics page
that calls SetNuiFocus(false, false) on the Lua side. Always release focus
in that same close path — don’t rely solely on the browser to signal closing,
since a crashed or reloaded browser frame won’t fire the event. Also release
focus in onClientResourceStop for your own resource, so a /restart can’t
leave input captured.
Customizing the loading screen
FiveM’s loading screen is a special NUI page that loads before any resource
starts and stays mounted until connection completes. It’s declared with
loadscreen in fxmanifest.lua instead of ui_page:
fx_version 'cerulean'
game 'gta5'
loadscreen 'html/loadscreen.html'
files {
'html/loadscreen.html',
'html/app.js'
}Because it runs before resources are available, it can’t rely on
SendNUIMessage from a client script the way a normal UI does. Instead, the
loading screen is event-driven: the game client posts connection-progress
information to the loading screen’s window as the player connects — data
file loading, resource download and initialization progress, and log-style
status lines. Listen for these the same way you’d listen for any NUI message,
and use the payload to drive your own progress bar or status text instead of
inventing a fixed step count.
// Field name below is illustrative — confirm the actual message shape for
// your target build against the docs linked underneath this example.
window.addEventListener('message', (event) => {
const data = event.data;
if (!data || typeof data.progress !== 'number') {
return;
}
document.querySelector('#progress').style.width = `${data.progress * 100}%`;
});Treat the exact set of connection-progress messages and their field names as something to confirm against the official loading-screen documentation for your target build rather than assuming a fixed list — the mechanism (event-driven progress data posted to the page) is stable, but the message set has grown over time.
A loading screen can also request loadscreen_manual_shutdown in its
manifest to control exactly when it dismisses, instead of the game closing it
automatically once connection finishes — useful for holding on a final
transition animation.