Assets & UI
FiveM NUI development — building in-game interfaces that feel native
How FiveM NUI really works: CEF quirks, the Lua-to-JS message contract, React and Next.js builds, SetNuiFocus, and the CSS that silently fails in-game.
What NUI actually is
NUI is an embedded Chromium (CEF) surface rendered on top of GTA V. Your resource declares an HTML file, the game loads it as a page, and you communicate with it in two directions: Lua sends messages into the page, and the page posts callbacks back to Lua over a local HTTP endpoint.
That means every skill you have as a web developer transfers. React, Vue, Svelte, Tailwind, animation libraries — they all work. What catches people out is that the CEF build shipped with the game is not the Chrome on your desktop, the page has no real network access by default, and the game continues to run underneath while your UI is open.
fx_version 'cerulean'
game 'gta5'
ui_page 'html/index.html'
client_scripts { 'client/*.lua' }
server_scripts { 'server/*.lua' }
files {
'html/index.html',
'html/**/*',
}The Lua ↔ JS message contract
The single most valuable thing you can do in a NUI project is decide, up front, on one message envelope and stick to it. Every bug I have debugged in someone else's NUI came from twenty different ad-hoc payload shapes.
local function send(action, data)
SendNUIMessage({ action = action, data = data })
end
send('setVisible', { visible = true })
send('setInventory', { items = items, weight = weight })type NuiMessage =
| { action: 'setVisible'; data: { visible: boolean } }
| { action: 'setInventory'; data: { items: Item[]; weight: number } }
export function useNuiEvent(handler: (msg: NuiMessage) => void) {
useEffect(() => {
const listener = (event: MessageEvent<NuiMessage>) => {
if (!event.data?.action) return
handler(event.data)
}
window.addEventListener('message', listener)
return () => window.removeEventListener('message', listener)
}, [handler])
}export async function fetchNui<T>(name: string, data?: unknown): Promise<T> {
const res = await fetch(`https://${GetParentResourceName()}/${name}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
body: JSON.stringify(data ?? {}),
})
return res.json()
}RegisterNUICallback('useItem', function(data, cb)
-- validate: the UI is a client, and clients lie
if type(data.slot) ~= 'number' then return cb({ ok = false }) end
TriggerServerEvent('inv:use', data.slot)
cb({ ok = true })
end)NUI focus — where the stuck-cursor bugs come from
`SetNuiFocus(hasFocus, hasCursor)` hands mouse and keyboard input to the browser layer. If your resource errors, restarts, or the player dies while focus is held, they are left with a cursor they cannot dismiss and a game they cannot control. Every serious NUI needs a guaranteed release path.
local open = false
local function setOpen(state)
open = state
SetNuiFocus(state, state)
SendNUIMessage({ action = 'setVisible', data = { visible = state } })
end
RegisterNUICallback('close', function(_, cb)
setOpen(false)
cb({})
end)
-- release focus if the resource stops for any reason
AddEventHandler('onResourceStop', function(res)
if res == GetCurrentResourceName() and open then
SetNuiFocus(false, false)
end
end)
-- and never keep focus through death
AddEventHandler('gameEventTriggered', function(name)
if name == 'CEventNetworkEntityDamage' and IsEntityDead(cache.ped) and open then
setOpen(false)
end
end)- Handle ESC inside the UI as well as in Lua — players expect it and it costs you three lines.
- Use `SetNuiFocusKeepInput(true)` when the player should still be able to move while the UI is open, such as a HUD with an editable field.
- Never leave focus on during a cutscene, a death screen or a teleport.
React, Vite and Next.js static export
The two setups worth using in 2026 are a Vite + React app, or a Next.js app configured for static export. Both give you a plain folder of HTML, JS and CSS that the game can load from disk.
// next.config.ts
const nextConfig = {
output: 'export',
distDir: '../html',
images: { unoptimized: true },
assetPrefix: './',
trailingSlash: true,
}
export default nextConfig- Relative asset paths are mandatory. The page is loaded from a `nui://` origin, so any absolute `/assets/...` path 404s.
- Keep the source in a `web/` folder and build into `html/`. Ship `html/` in the resource, keep `web/` for development.
- Develop against a browser mock — stub `fetchNui` to return fake data when `window.invokeNative` is undefined. You get hot reload and real devtools.
- Never point `ui_page` at a localhost dev server on a live server. It works on your machine and nowhere else.
const isBrowser = !(window as any).invokeNative
export async function fetchNui<T>(name: string, data?: unknown, mock?: T) {
if (isBrowser) return mock as T
const res = await fetch(`https://${GetParentResourceName()}/${name}`, { /* ... */ })
return res.json() as Promise<T>
}CSS that silently fails inside the game
This is the section that saves people days. FiveM's CEF build does not support everything your desktop Chrome does, and it fails silently — the property is ignored and your beautiful frosted panel renders as a flat rectangle.
| Feature | Status in FiveM CEF | What to do instead |
|---|---|---|
| backdrop-filter | Unreliable — often ignored entirely | Use a solid semi-transparent colour (rgba) for the panel background |
| Web fonts from a CDN | No network access by default | Ship the font files in the resource and @font-face them locally |
| position: fixed with transforms | Works, but layering with the game surface can surprise you | Keep a single fixed root and position everything inside it |
| Very large CSS animations | Works, but costs real frame time | Animate transform and opacity only; avoid animating layout properties |
| localStorage | Works, but is wiped by cache clears | Persist anything that matters through Lua and the server |
Keeping the UI cheap
- Never re-render the whole UI on every HUD tick. Push values into a store and let only the affected component subscribe.
- Throttle high-frequency updates from Lua. A speedometer does not need 60 messages a second — 10 is indistinguishable.
- Unmount hidden UIs rather than hiding them with CSS. A hidden React tree still reconciles.
- Avoid heavy blur, large box-shadows and full-screen filters; they are expensive on the compositing path.
- Measure with the CEF devtools — open them with the FiveM console command and profile like any web app.
Ship checklist
- Focus is released on close, on death, on resource stop and on ESC.
- Every RegisterNUICallback validates its input and always calls cb.
- All assets load relatively and no request leaves the machine.
- The UI renders correctly at 1920×1080, 2560×1440 and ultrawide.
- Text scales sensibly — use rem/vh units, not fixed pixels, or the UI is unusable at 4K.
- resmon shows the resource near zero when the UI is closed.
Frequently asked questions
Can I use React for FiveM NUI?
Yes — React is the most common choice for serious FiveM interfaces. Build it with Vite or Next.js static export into a plain HTML/JS folder, ship that folder in the resource, and point ui_page at the built index.html.
Why is my FiveM NUI cursor stuck on screen?
SetNuiFocus was left enabled. Add release paths for resource stop, player death and ESC, and make sure every close path calls SetNuiFocus(false, false).
Does backdrop-filter work in FiveM?
Not reliably. The CEF build shipped with FiveM frequently ignores it, so a glass panel renders flat. Use solid rgba backgrounds and a hairline border to get the same visual weight.
Why do my NUI images not load?
Almost always absolute paths. The page is served from a nui:// origin, so /images/logo.png resolves nowhere. Use relative paths and set assetPrefix to './' in your build config.
Need this built, not just explained?
Ten years of FiveM work, from Lua to NUI
Custom resources, React NUI, ESX / QBCore / Qbox integration, OneSync performance audits and security reviews — plus the websites and SEO around your server brand.
Related guides
- EngineeringFiveM server optimizationRead resmon properly, kill per-frame loops, tune OneSync and entity limits, and find the database query that is freezing your server.
- FrameworksESX vs QBCore vs QboxData models, inventories, script ecosystems, performance and hiring cost — the real differences between the three FiveM frameworks.
- Assets & UIFiveM loading screen guideHow loadscreen resources work, adding music and video, live player counts, and installing one without breaking your spawn.