Skip to content

UI / NUI conventions

Each resource has its own NUI page (ui_page 'html/index.html'). bs_ui is the HUD, the notifications and the shared bits.

One panel at a time: bs:ui:opened / bs:ui:closed

Section titled “One panel at a time: bs:ui:opened / bs:ui:closed”

When your panel opens you announce it. Every other panel hears it and closes itself. When yours closes, you announce that too. Real code from bs_config:

local function close()
if not isOpen then return end
isOpen = false
SetNuiFocus(false, false)
SendNUIMessage({ action = 'close' })
TriggerEvent('bs:ui:closed', 'config')
end
local function open()
if isOpen then return close() end
isOpen = true
SetNuiFocus(true, true)
SendNUIMessage({ action = 'open', data = data })
TriggerEvent('bs:ui:opened', 'config')
end
-- another UI took over: step aside
AddEventHandler('bs:ui:opened', function(what)
if what ~= 'config' and isOpen then close() end
end)
AddEventHandler('onResourceStop', function(res)
if res == GetCurrentResourceName() and isOpen then
SetNuiFocus(false, false)
TriggerEvent('bs:ui:closed', 'config')
end
end)

Rules:

  • Pass your panel name in both events. Handlers check it, a handler that reacts to any close broke the field map for months.
  • Pair them on every exit: close button, Esc, toggle key, another UI opening, resource stop. Miss one and the player is stuck with a mouse cursor and no game.
  • Announce opened after you are open, and close yourself from your own handler, don’t assume others will.

/refreshui (bs_ui) is the panic button: it closes everything, drops focus and fires bs:ui:closed with nil.

-- client
TriggerEvent('bs:ui:notify', { type = 'info', title = 'Radio', message = 'Signal found.', duration = 3000 })
-- server
TriggerClientEvent('bs:ui:notify', src, { type = 'error', title = 'Denied', message = 'Nope.' })

Types used around the code: info, success, error.

Pages are laid out for 1920x1080. Include the shared scaler so they fit any resolution:

<script src="https://cfx-nui-bs_ui/html/shared/scale.js"></script>

It scales <body> with a transform: --ui-scale = clamp(min(w/1920, h/1080), 0.6, 1.25). Players can override it (0.6 to 1.4) in settings.

Things to know when you use it:

  • In page CSS write vh/vw as calc(Nvh / var(--ui-scale, 1)), so they stay real viewport fractions.
  • JS that sets style.left/top from clientX/clientY or a bounding rect must divide by BSScale.get(). BSScale.w() / BSScale.h() give the local viewport.
  • Opt out with <html data-bs-noscale>.
  • html, body { background: transparent !important; } on every page. Otherwise you get a black screen over the game.
  • No NUI messages every frame while idle. Push on change, or throttle (the drone HUD sends every 150 ms while flying, that’s about the max).
  • Palette is amber / rust / bone / dust on near-black. Reuse the bs_ui CSS tokens and bs_menu/html/css/faction-hub.css. No external fonts or CDNs, players may not have internet access to them and it’s a privacy thing anyway.
  • Icons: line icons in bs_ui/html/js/icons.js.
  • Sounds: bs_ui/html/js/sound.js handles positional sounds (maxDistance, panning).