Skip to content

NUI Tier 2 — Full NUI Apps

Tier 2 NUI is for complex, custom UIs: phone apps, inventory grids, maps, and HUD elements. You write the HTML/CSS/JS; Shiva provides the Lua bridge.

Structure

my-module/
├── ui/
│   ├── index.html
│   ├── src/
│   │   └── App.vue   (or App.tsx, App.svelte, etc.)
│   ├── package.json
│   └── vite.config.ts

Setting Up the UI

bash
cd resources/[shiva]/my-module
shiva make ui        # scaffolds the ui/ directory
cd ui && npm install

The Lua ↔ JS Bridge

Lua → JS (show data)

lua
-- Lua (client side)
SendNUIMessage({
    action = 'open',
    data   = {
        items = playerItems,
    },
})

SetNuiFocus(true, true)
js
// JavaScript
window.addEventListener('message', (event) => {
    if (event.data.action === 'open') {
        store.items = event.data.data.items
        isOpen.value = true
    }
})

JS → Lua (send actions)

js
// JavaScript: send a callback to Lua
async function closeInventory() {
    await fetch(`https://my-module/close`, {
        method: 'POST',
        body: JSON.stringify({}),
    })
}
lua
-- Lua: register the callback
RegisterNUICallback('close', function(data, cb)
    SetNuiFocus(false, false)
    cb({ ok = true })
end)

Building for Production

bash
cd ui && npm run build

The built assets go to ui/dist/. Reference them in fxmanifest.lua:

lua
ui_page 'ui/dist/index.html'

files {
    'ui/dist/**',
}

Development with Hot Reload

bash
cd ui && npm run dev
# Then in game: set the NUI url to http://localhost:5173

Security Checklist

  • [ ] Never expose sensitive server-side data in NUI state
  • [ ] Validate all RegisterNUICallback inputs on the server
  • [ ] Don't trust display values (balances, inventory counts) for authority
  • [ ] Set SetNuiFocus(false, false) on disconnect/resource stop

See Also

Released under the MIT License.