Skip to main content
TRACK B·INTERFACES (NUI)·Verified June 2026 · Lua 5.4 · ox_lib 3.x
Learning with an AI assistant?
Copies this lesson plus 2026 ground rules (no lua54 'yes', Cfx.re Portal, correct callback signatures) as a ready-to-paste mentor prompt.

Build your first NUI with HTML, CSS, and JavaScript

NUI is FiveM's name for the in-game browser layer, a real Chromium tab the client renders on top of GTA. Every menu, hud, scoreboard, and pause UI you have ever seen in a server is NUI. So far you have used ox_lib and other framework UIs that wrap NUI for you. (ox_lib is a community library from the Overextended team, not a Cfx.re built-in, but it is everywhere.) This bonus shows the layer underneath: how to declare a page in fxmanifest.lua, how to push state from Lua with SendNUIMessage, how to receive clicks with RegisterNUICallback, and how to capture the cursor with SetNuiFocus. Once you understand these four pieces, you can read any NUI resource and build your own without a framework.

You'll learn
The GetParentResourceName callback URL, the Lua to JS message and callback APIs, focus capture, opening a menu from a key press, and how to attach Chrome devtools to a live NUI page.
You'll ship
A minimal NUI menu: a command (or a key press) opens a Chromium overlay with one button, the button sends data back to Lua, and a Lua-triggered chat message confirms the round-trip.
Time
~30 minutes
Why now
You finished the vehicle handling lesson. Knowing how NUI works under the hood lets you debug any UI bug for the rest of the course.
Quasar School: NUI with a command (part 1).
Build a lockpicking skill-check NUI, end to end.
BEFORE YOU START

Build it

Make the resource folder

The server has one folder for this lesson.

Inside your server's resources folder, create this folder:

code
resources/qu_nui_from_scratch

Create the files

Every file named in the manifest exists.

Create this exact file layout:

code
resources/qu_nui_from_scratch/
fxmanifest.lua
client.lua
web/index.html
web/app.js
web/style.css

Write fxmanifest.lua

FiveM knows which files to load.

Open fxmanifest.lua and paste this:

code
fx_version 'cerulean'
game 'gta5'

client_script 'client.lua'

ui_page 'web/index.html'

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

Older tutorials add a lua54 'yes' line here. As of June 2025 that setting is deprecated and ignored: Lua 5.4 is the only Lua runtime, so you leave it out.

Write the lesson code

The topic is now represented by runnable code.

Open client.lua and paste this:

code
RegisterCommand('openui', function()
SetNuiFocus(true, true)
SendNUIMessage({ action = 'open', title = 'NUI from scratch' })
print('[qu_nui_from_scratch] opened, focus captured')
end, false)

RegisterNUICallback('buttonClicked', function(data, cb)
SetNuiFocus(false, false)

local label = data.label or 'unknown'
print('[qu_nui_from_scratch] callback received, label = ' .. label)

TriggerEvent('chat:addMessage', {
    color = { 0, 200, 120 },
    args = { 'NUI', 'Round-trip complete. JS sent: ' .. label }
})

cb({ ok = true })
end)

Open web/index.html and paste this:

code
<!doctype html>
<html>
<head><link rel="stylesheet" href="style.css" /></head>
<body>
<div id="menu" class="hidden">
  <h1 id="title">menu</h1>
  <button id="action">Send to Lua and close</button>
</div>
<script src="app.js"></script>
</body>
</html>

Open web/app.js and paste this:

code
const menu = document.getElementById('menu');
const title = document.getElementById('title');
const button = document.getElementById('action');

// Lua to JS: reveal the menu only when Lua pushes the open message.
window.addEventListener('message', (event) => {
if (event.data.action === 'open') {
    title.textContent = event.data.title;
    menu.classList.remove('hidden');
}
});

// JS to Lua: send a real payload back, then hide on the callback's reply.
button.addEventListener('click', () => {
fetch(`https://${GetParentResourceName()}/buttonClicked`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ label: 'Send to Lua and close' })
}).then(() => {
    menu.classList.add('hidden');
});
});

Open web/style.css and paste this:

code
body { margin: 0; display: grid; place-items: center; min-height: 100vh; font-family: sans-serif; }
#menu { background: #14161a; color: #fff; padding: 28px; border-radius: 10px; text-align: center; }
.hidden { display: none; }

Start and test it

The expected proof appears in F8 and in chat.

Open server.cfg and add this line:

code
ensure qu_nui_from_scratch

Save, then run this in your server console (the FXServer window, or the txAdmin Live Console if you run txAdmin):

code
restart qu_nui_from_scratch

Now join the server. Run this test from the in-game chat box (press T to open it, type the command, press Enter):

code
/openui, then click "Send to Lua and close"

And in the chat box you see one line: NUI: Round-trip complete. JS sent: Send to Lua and close. That chat line is the proof the data made it from the browser back into Lua, not just an empty request.

Keep reading the full lesson

Sign in to start, then unlock every step of this lesson and the full FiveM School with a membership.

Still ahead in this lesson
  • How it works
  • A different NUI entry point: the loading screen
  • If something went wrong
  • What you can do now
  • Try it yourself

The remainder of Build your first NUI with HTML, CSS, and JavaScript is available to FiveM School members.