<!-- fivemad.com, Markdown source of https://fivemad.com/docs/development/nui -->
> Source: https://fivemad.com/docs/development/nui
> Section: Development
> Tags: nui, html, javascript, react, SendNUIMessage, RegisterNUICallback, SetNuiFocus, ui
> Last updated: 2026-09-22
> Site map for assistants: https://fivemad.com/llms.txt

# NUI basics: HTML interfaces in FiveM

Build FiveM UIs with NUI: ui_page and files, SendNUIMessage, RegisterNUICallback with fetch, SetNuiFocus, dev tools, React or Vue with a build step, and common pitfalls.

**NUI** is FiveM's built in Chromium (CEF) browser. Every resource can have a web page that sits on top of the game: HUDs, menus, phones, inventories, loading screens. You write it with HTML, CSS and JavaScript, or any framework that builds to static files (React, Vue, Svelte, Solid...).

[How to Create HUDs and UIs using NUI (CharlesHacks, Zero to Hero Ep. 7)](https://www.youtube.com/watch?v=Z2yUTkE94pQ) (YouTube video)

## How it fits together

```text
 Lua client script  ── SendNUIMessage({...}) ──►  your page (window 'message' event)
 Lua client script  ◄── RegisterNUICallback ────  fetch('https://<resource>/<name>')
```

- Lua sends data **to** the page with `SendNUIMessage`.
- The page sends data **back** with an HTTP style `fetch` to `https://<resource-name>/<callback-name>`, which triggers a `RegisterNUICallback` handler in Lua.
- `SetNuiFocus` decides whether the page gets the mouse and keyboard.
- The page never talks to the server directly. If the server needs to know, Lua forwards it with an event.

## A minimal example

```text
nui_demo/
├─ fxmanifest.lua
├─ client.lua
└─ html/
   ├─ index.html
   ├─ style.css
   └─ app.js
```

```lua title="fxmanifest.lua"
fx_version 'cerulean'
game 'gta5'

client_script 'client.lua'

ui_page 'html/index.html'

files {
    'html/index.html',
    'html/style.css',
    'html/app.js',
}
```

Every file the page loads must be listed in `files`, or the client never downloads it and you get a blank or broken UI.

```html title="html/index.html"
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div id="panel" class="hidden">
    <h1 id="title">Hello</h1>
    <button id="close">Close</button>
  </div>
  <script src="app.js"></script>
</body>
</html>
```

```css title="html/style.css"
html, body { margin: 0; background: transparent; font-family: sans-serif; }
.hidden { display: none; }
#panel {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  padding: 24px; background: rgba(15, 15, 15, 0.9); color: #fff; border-radius: 8px;
}
```

Keep the `body` background **transparent**, or the page covers the game.

```js title="html/app.js"
const panel = document.getElementById('panel');
const title = document.getElementById('title');

window.addEventListener('message', (event) => {
  const data = event.data;
  if (data.action === 'open') {
    title.textContent = data.title;
    panel.classList.remove('hidden');
  } else if (data.action === 'close') {
    panel.classList.add('hidden');
  }
});

function post(name, body = {}) {
  return fetch(`https://${GetParentResourceName()}/${name}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json; charset=UTF-8' },
    body: JSON.stringify(body),
  }).then((r) => r.json());
}

document.getElementById('close').addEventListener('click', () => post('close'));

document.addEventListener('keyup', (e) => {
  if (e.key === 'Escape') post('close');
});
```

`GetParentResourceName()` is available inside NUI pages and returns your resource name, so the callback URL stays right if someone renames the folder.

```lua title="client.lua"
local open = false

local function setOpen(state)
    open = state
    SetNuiFocus(state, state)          -- keyboard focus, mouse cursor
    SendNUIMessage({ action = state and 'open' or 'close', title = 'Hello from Lua' })
end

RegisterCommand('demo', function()
    setOpen(not open)
end, false)

RegisterNUICallback('close', function(data, cb)
    setOpen(false)
    cb({ ok = true })                  -- always answer, or fetch() hangs
end)
```

`ensure nui_demo`, type `/demo`, and the panel appears with a mouse cursor. <kbd>Esc</kbd> or the button closes it.

## Focus

`SetNuiFocus(hasFocus, hasCursor)`:

- `SetNuiFocus(true, true)`: the page gets keyboard and mouse. The player can't move or use game controls. Use for menus.
- `SetNuiFocus(false, false)`: the page is display only (HUDs). The game keeps control.
- `SetNuiFocusKeepInput(true)` lets the game still receive keyboard input while the page has focus, for UIs where the player keeps walking. Disable the controls you don't want yourself.

> [!WARNING]
> Always give the player a way out (<kbd>Esc</kbd>, a close button) and release focus on close. A UI that keeps focus traps the player, who has to quit the game. Also release focus on `onResourceStop`.

If your UI opens with <kbd>Tab</kbd> (inventories do), check `IsNuiFocused()` first so it doesn't steal focus from another open UI like the txAdmin menu.

## Debugging NUI

Type `nui_devtools` in the F8 console to open Chrome DevTools for NUI: console logs, element inspector, network tab. Errors in your JavaScript show there, not in F8.

Other tips:

- Develop the page in a normal browser first, faking the `message` events with `window.postMessage({ action: 'open', title: 'test' })`.
- `ui_page` can point to a dev server URL (`ui_page 'http://localhost:5173'`) while you work, so hot reload works. Switch back to the built file before you ship.

## React, Vue and friends

Frameworks work fine. You build to static files and point `ui_page` at the built `index.html`:

```lua title="fxmanifest.lua"
ui_page 'web/build/index.html'

files {
    'web/build/index.html',
    'web/build/**/*',
}
```

- Use **relative** asset paths in your build (Vite: `base: './'`), because the page is served from `https://cfx-nui-<resource>/`.
- Starters save setup time: [project-error/fivem-react-boilerplate-lua](https://github.com/project-error/fivem-react-boilerplate-lua) (React + TypeScript + Lua) and [overextended/fivem-typescript-boilerplate](https://github.com/overextended/fivem-typescript-boilerplate).
- Ship the **built** folder in releases. Server owners shouldn't need Node to run your resource.

## Loading screens

A loading screen is a special NUI page shown while the player joins:

```lua title="fxmanifest.lua"
loadscreen 'html/index.html'
files { 'html/**/*' }
-- optional: keep it until you close it yourself
loadscreen_manual_shutdown 'yes'
```

With manual shutdown, call `ShutdownLoadingScreenNui()` from a client script when your spawn is ready, or players stay on the loading screen forever.

## Performance

- NUI pages that animate constantly (CSS animations, requestAnimationFrame, big videos) cost GPU and CPU on every client. Pause them when hidden.
- Don't `SendNUIMessage` every frame. Send when data changes, or a few times per second for HUD values.
- Keep images reasonable in size. Every file in `files` is downloaded by every player.

## Common mistakes

| Symptom | Cause |
| --- | --- |
| Blank UI | `ui_page` or its assets not listed in `files`, wrong path, or absolute asset paths in a framework build. |
| `fetch` never resolves | The Lua `RegisterNUICallback` handler didn't call `cb()`. |
| UI shows but clicks do nothing | `SetNuiFocus(true, true)` not called. |
| Player stuck with a cursor | Focus not released on close or on resource stop. |
| White or black background over the game | `body` or `html` background isn't transparent. |
| Works in browser, not in game | Using APIs NUI doesn't allow, or the callback URL doesn't match the resource name. Use `GetParentResourceName()`. |

Next: [Exports and dependencies](/docs/development/exports-dependencies).
