Skip to Content
ScriptingAdvanced NUI

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 input

When 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 SendNUIMessage and let pointer-events: none in 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.