Skip to content

Creating menus ​

A menu is a Lua table with a header and a list of items. You register it once, then open it by id. The definition, callbacks, permissions and the back stack stay in Lua. The web only gets a serialized copy without functions.

Register and open ​

lua
exports.np_menu:register({
  id = 'mechanic',                    -- required, unique
  title = 'Mechanic', subtitle = 'LS Customs', icon = 'wrench', tint = 'orange',
  items = {
    { type = 'button', label = 'Repair vehicle', icon = 'screwdriver-wrench',
      onSelect = function(item, menu) print('repair') end },
    { type = 'submenu', label = 'Parts', icon = 'gears', menu = 'mechanic_parts' },
  },
})

exports.np_menu:open('mechanic')

Registering an id again replaces the definition and re-renders it if it's visible. open(id, { selectedId = 'rowId' }) selects a row on open. open(id, { keepStack = true }) pushes the menu on top of the open one, so Backspace goes back to it (or, if id is already in the stack, pops back down to it).

FieldDefaultDescription
idRequired. Unique menu id
title, subtitleHeader text. The subtitle is shown in capitals
icon, tintHeader tile. Icons are Font Awesome 6 solid names without fa-. Tints: neutral, red, orange, rust, pink, olive, lilac, green, blue, aqua, brand
layout'list''list' or 'grid' (phone home-screen pages with a dock)
positionplayer setting'right', 'left' or 'center'
skin'default''staff' uses the red-edged danger glass for admin tools
searchtrue for listsType-to-filter row (/ or Tab focuses it)
searchDeepfalseAlso search items of submenus registered with menu = 'id' (not build). Results show their breadcrumb
countertrue for listsThe 4 / 14 position tag
canClosetruefalse: Backspace on the root and the close button do nothing
dockGrid only: items shown in the dock. Pinned items are appended
preview, cartPreview card and cart
tag{ icon, label } meta tag in the header (the quick menu shows the clock)
allowDeadfalseKeep the menu open (and allow opening it) while the player is dead
onOpen(menu)After the menu is shown
onClose(menu, reason)For every menu in the stack when it closes. reason: 'api', 'back', 'select', 'user', 'replaced', 'removed', 'death', 'server', 'stop', …
onBack(menu)When this level is popped with Backspace
onFocus(item, menu)The selection moved to item
onCart(menu)The cart button was pressed
items{}Rows, see below

Item fields (all types) ​

FieldDescription
idOptional, unique per menu. Missing ids become i1, i2, … Give rows an id if you want to update them later
typeOne of the item types. Defaults to submenu when menu / build is set, else button
label, sublabelRow text and the small second line
descriptionShown in the description card while the row is selected. **bold** is supported
icon, tintRow tile. Colour means something: green confirm, red destructive, orange warning, blue police…
keywordsExtra search terms
disabled, disabledReasonGreyed out. Disabled rows stay selectable so the reason is visible; Enter plays the error sound
loadingSpinner instead of the control. Set automatically while a server action runs
badgeNumber, short string, or true for a dot
cooldownms, or { remaining = ms, total = ms }. Shows a countdown bar; the row can't be used until it ends
visiblefalse or a function (item, menu) -> bool to hide the row
ace, groups, jobs, canInteract, showLockedPermissions, see below
onSelect(item, menu, value)Enter on the row. Return false to cancel the close / server action that would follow
onChange(value, item, menu)The value changed. Return false to revert it
onFocus(item, menu)The row was selected
server, args, onResultCall a server action after onSelect / onChange
confirmtrue or { title, message, confirmLabel, cancelLabel, hold, timeout }: ask first
closetrue: close the whole menu after onSelect

Callbacks run in a thread, so they can block: open a dialog, call the server, Wait.

The OOP wrapper ​

Add the wrapper to your resource's fxmanifest.lua:

lua
shared_script '@np_menu/import.lua'

It defines a global NpMenu table in your resource:

lua
local m = NpMenu.create({ id = 'dev_tools', title = 'Tools', icon = 'code', tint = 'orange' })

m:add({ type = 'switch', id = 'god', label = 'Godmode', icon = 'shield-halved',
  onChange = function(on) SetEntityInvincible(PlayerPedId(), on) end })
m:add({ type = 'button', id = 'tp', label = 'Teleport to waypoint', icon = 'location-dot' }, 1) -- insert at index 1

m:open()
m:update('god', { checked = true })        -- patch one row
m:update({ subtitle = 'Updated' })        -- patch the header
m:remove('tp')                            -- remove a row; m:remove() unregisters the menu
m:close()
MethodMaps to
NpMenu.create(def)register(def), returns the object (or nil)
m:add(item, index?)addItem
m:set(items)setItems
m:update(itemId, patch) / m:update(patch)updateItem / update
m:remove(itemId?)removeItem / remove
m:get(itemId)getItem (serialized copy)
m:open(opts?), m:close(reason?), m:isOpen()open, close (only if this menu is in the stack), inStack
m:preview(card), m:cart(cart)setPreview, setCart

The wrapper also forwards NpMenu.inputDialog, confirm, notify, addRadial, removeRadial, showContext, closeContext, openRadial, closeRadial, callServer and getSettings. On the server it provides NpMenu.registerAction(name, opts) and NpMenu.notify(src, data).

Examples per item type ​

See Item types for every field.

lua
items = {
  { type = 'section', label = 'Actions' },
  { type = 'button', label = 'Open inventory', icon = 'box-open', key = 'F2', rightLabel = 'Tab' },
  { type = 'action', variant = 'confirm', label = 'Spawn vehicle', icon = 'check', tint = 'green',
    onSelect = function() spawn() end },
  { type = 'action', variant = 'danger', hold = 1200, label = 'Delete vehicle', sublabel = 'Hold to confirm',
    icon = 'trash', tint = 'red', onSelect = function() deleteVehicle() end },
  { type = 'action', variant = 'warning', label = 'Kick', icon = 'door-open', tint = 'orange',
    confirm = { title = 'Kick player?', message = 'They will be **disconnected**.' } },
  { type = 'separator' },

  { type = 'section', label = 'Values' },
  { type = 'info', label = 'Street', icon = 'location-dot', value = 'Elgin Ave' },
  { type = 'number', label = 'Amount', icon = 'hashtag', value = 1, min = 1, max = 50, step = 1, bigStep = 10 },
  { type = 'list', label = 'Weather', icon = 'cloud-sun', options = { 'Clear', 'Rain', { label = 'Thunder', value = 'THUNDER' } },
    index = 1, onChange = function(index, item) print(item.options[index]) end },
  { type = 'slider', label = 'Time of day', icon = 'clock', min = 0, max = 1439, step = 1, bigStep = 15, value = 720,
    format = function(v) return ('%02d:%02d'):format(v // 60, v % 60) end },
  { type = 'color', label = 'Marker colour', icon = 'palette', value = '#F2F2F2', palette = { '#F2F2F2', '#E05A1E', '#2C4EA0' } },
  { type = 'input', label = 'Note', icon = 'pen', placeholder = 'Enter text…', maxLength = 64 },
  { type = 'input', inputType = 'money', label = 'Price', icon = 'dollar-sign', tint = 'green', value = 25, min = 0, unit = '$' },
  { type = 'keybind', label = 'Open inventory', icon = 'keyboard', value = 'F2' },

  { type = 'section', label = 'Toggles' },
  { type = 'switch', label = 'Noclip', icon = 'ghost', checked = false, onChange = function(on) setNoclip(on) end },
  { type = 'checkbox', label = 'Shirt', icon = 'shirt', checked = true },
  { type = 'radio', group = 'walk', label = 'Default', checked = true },
  { type = 'radio', group = 'walk', label = 'Gangster', onChange = function() setWalk('gangster') end },

  { type = 'submenu', label = 'Vehicle', sublabel = 'Spawn, repair, tuning', icon = 'car', count = 12, menu = 'dev_vehicle' },
  { type = 'link', label = 'Open phone', icon = 'mobile-screen', onSelect = function() ExecuteCommand('phone') end },
}
  • Values are stored back into the item, so reopening the menu shows the latest value. format(value, item, menu) returns the text shown instead of the raw value.
  • Radio rows only fire onChange(true) on the newly checked row. np_menu unchecks the rest of the group in that menu.
  • Links close the menu after onSelect unless close = false.

Point a submenu row at a registered menu, or build it when the player enters it:

lua
-- 1. a registered menu (searchable with searchDeep)
{ type = 'submenu', label = 'Parts', menu = 'mechanic_parts' }

-- 2. built on enter: return an items array, or a full definition
{ type = 'submenu', label = 'Players', icon = 'users',
  build = function(childId, item, parent)
    local items = {}
    for _, id in ipairs(GetActivePlayers()) do
      items[#items + 1] = { type = 'button', label = GetPlayerName(id), icon = 'user' }
    end
    return items   -- or { title = 'Online', subtitle = '…', items = items }
  end }

A built submenu is registered as a temporary menu <parentId>/<itemId> and dropped when the player backs out. It inherits the parent's skin, position and search setting, and the row's label, icon and tint as its header.

Updating a menu ​

lua
exports.np_menu:updateItem('mechanic', 'price', { value = 450, badge = 'NEW' })  -- patch one row
exports.np_menu:updateItem('mechanic', 'price', { badge = false })               -- `false` clears a field
exports.np_menu:update('mechanic', { subtitle = 'Closed', tint = 'red' })         -- header fields
exports.np_menu:setItems('mechanic', newItems)                                    -- replace all rows
exports.np_menu:addItem('mechanic', { type = 'button', label = 'New' }, 2)        -- insert at index 2
exports.np_menu:removeItem('mechanic', 'price')

Only the changed row is sent to the web. Changing visible, ace, groups, canInteract or showLocked re-sends the whole list so permissions are evaluated again.

Permissions ​

FieldCheck
ace = 'np_menu.admin' or a listIsPlayerAceAllowed on the server. The client asks once per ace and caches the answer, so the row appears after the first answer arrives
groups = 'police', { 'police', 'ems' } or { police = 2 }Framework job / gang (qbx HasGroup), with a minimum grade in the map form. jobs is checked the same way on the client
canInteract = function(item, menu) return ok, reason endAnything else
showLocked = trueShow a failing row as disabled with the reason, instead of hiding it

These checks only decide what the player sees. Anything that matters must be a server action, which checks again on the server.

Preview and cart ​

Shops show a preview card next to the menu and a cart footer.

lua
exports.np_menu:setPreview('my_shop', {
  title = 'Karin Futo', subtitle = 'Preview · Engine EMS 3', icon = 'car',
  image = 'nui://my_shop/images/futo.png',   -- must match Config.ImageHosts
  hint = 'Drag to rotate · Scroll to zoom',
  stats = {
    { label = 'Top speed', value = 68, delta = 6 },       -- value 0..100, delta -100..100
    { label = 'Braking', value = 48, display = '48' },
  },
  palette = { label = 'Primary colour', colors = { '#F0F0F0', '#0D1116', '#E05A1E' }, selected = 3 },
})

exports.np_menu:setCart('my_shop', { label = '2 changes in cart', total = '$12,400', action = 'Buy' })
exports.np_menu:update('my_shop', { onCart = function(menu) buy() end })

exports.np_menu:setPreview('my_shop', nil)   -- remove

Pressing the cart button calls the menu's onCart(menu) (or cart.onSelect). Use onFocus on the rows to update the preview while the player scrolls.

Dialogs ​

Both dialogs block the calling thread until the player answers. Only one dialog can be open: a second call returns nil / false right away.

lua
local values = exports.np_menu:inputDialog({
  title = 'Create invoice', message = 'Every field is **validated**.', icon = 'file-invoice', tint = 'blue',
  confirmLabel = 'Send invoice',
  fields = {
    { id = 'target', type = 'text', label = 'Recipient', placeholder = 'Name or ID', required = true },
    { id = 'amount', type = 'money', label = 'Amount', min = 1, max = 100000, required = true },
    { id = 'qty', type = 'number', label = 'Quantity', default = 1, min = 1 },
    { id = 'pin', type = 'password', label = 'Account PIN' },
    { id = 'account', type = 'select', label = 'Account', default = 'bank',
      options = { { label = 'Cash', value = 'cash' }, { label = 'Bank', value = 'bank' } } },
    { id = 'colour', type = 'color', label = 'Label colour', default = '#465E8A' },
    { id = 'notes', type = 'textarea', label = 'Notes', maxLength = 500, hint = 'Optional' },
    { id = 'mail', type = 'checkbox', label = 'Send a receipt by mail', default = true },
  },
})
if values then print(values.target, values.amount, values.account) end   -- nil when cancelled

local ok = exports.np_menu:confirm({
  title = 'Delete outfit "Work"?', message = "This can't be undone.", icon = 'trash', tint = 'red',
  variant = 'danger', confirmLabel = 'Delete', cancelLabel = 'Keep',
  timeout = 10000,   -- auto-cancel after 10 s (confirm only)
  hold = 800,        -- hold the confirm button for 800 ms
})

Field types: text, number, money (whole numbers), password, select, checkbox, color, textarea. Lua validates the submitted values again: numbers are clamped to min / max, a select value must be one of the options, and the dialog stays open while a required field is empty. confirm('Are you sure?') also accepts a plain string.

Input dialogConfirm
Input dialogConfirm

Notifications ​

lua
exports.np_menu:notify({ type = 'success', title = 'Saved', message = 'Outfit **Work** saved.', icon = 'shirt',
  tint = 'pink', duration = 4000 })
exports.np_menu:notify('Vehicle locked', 'info')   -- short form: title, type

type: info, success, warning, error. Optional app sets the small label in the toast head. From the server: exports.np_menu:notify(source, data).

Toasts

Events ​

np_menu fires local client events that other resources can listen to:

EventArguments
np_menu:openedmenuId
np_menu:closedrootMenuId, reason
np_menu:selectedmenuId, itemId, value
np_menu:changedmenuId, itemId, value
np_menu:radialOpenedsessionId
np_menu:contextOpenedmenuId
np_menu:contextClosedmenuId, reason
np_menu:settingChangedkey, value
lua
AddEventHandler('np_menu:opened', function(id) if id == 'mechanic' then TriggerEvent('my_hud:hide') end end)

np_* FiveM resources