The context menu is the main vertical menu type. It supports a rich set of interactive items, real-time reactive updates, accordion sections, tabs, pagination, search, keyboard navigation, drag positioning, and a hover preview panel.
- Opening a menu
- Meta options
- Item types
- Real-time reactive items
- Reusable menus
- Sub-menus
- Live rebuild (
context_update) - User settings
- Security notes
local UI = exports['LastMenu']
-- One-shot: opens immediately, closed on callback or Escape
UI:context(function(menu)
menu:title("My Menu")
menu:button("Click me", { cb = function() print("clicked") end })
end)Call these at the top of your builder function, before adding items.
| Method | Type | Default | Description |
|---|---|---|---|
menu:title(str) |
string | '' |
Title displayed in the header |
menu:banner(url) |
string | nil | Image URL shown above the title (https:// or nui://) |
menu:description(txt) |
string | nil | Subtitle shown below the title |
menu:animation(anim) |
string | 'slideLeft' |
Open animation: 'slideLeft' 'slideRight' 'fade' 'scale' |
menu:nav(mode) |
string | 'both' |
Force nav mode: 'mouse' 'keyboard' 'both' |
menu:search() |
— | — | Force-show the search bar (auto-shown when items exceed page_size) |
menu:page_size(n) |
number | 20 | Items per page (pagination auto-activates above this). Ignored when scroll() is set. |
menu:scroll() |
— | — | 1.0.0 Disable pagination — render all items with native scroll. Mutually exclusive with page_size(). |
menu:cancelable(v) |
bool | true |
Set to false to prevent Escape / B-button from closing this menu. |
UI:context(function(menu)
menu:title("Garage")
menu:banner("https://i.imgur.com/example.jpeg")
menu:description("Repair and customize your vehicle.")
menu:animation("slideRight")
menu:nav("both") -- respect user setting
menu:page_size(10)
end)Call menu:scroll() to disable pagination and render all items in a scrollable container instead. Useful for long lists where pagination adds friction.
UI:context(function(menu)
menu:title("Inventaire")
menu:scroll() -- all items visible at once, container scrolls
for i = 1, 50 do
menu:button("Item #" .. i, { cb = function() end })
end
end)Keyboard navigation (
↑/↓) scrolls the container automatically to keep the focused item visible.
The most common item. Supports icons, badges, hotkeys, gradient backgrounds, hold-to-confirm, cooldown, and a hover preview panel.
menu:button(label, opts)| Option | Type | Description |
|---|---|---|
icon |
string | Lucide icon name (e.g. "wrench", "car", "zap") |
color |
string | Accent color for the icon and gradient (hex) |
gradient |
bool | Show a gradient background using color |
badge |
string | Small badge on the right (e.g. "NEW", "500€") |
hint |
string | Dim text on the right (e.g. "E", "500€") |
hotkey |
string | Keyboard shortcut label shown as <kbd> |
arrow |
bool | Show a › arrow (indicates a sub-menu) |
confirm_hold |
bool | number | true = hold 1.5s to confirm; a number specifies a custom duration in ms (e.g. 3000) |
cooldown |
number | Milliseconds before button can be clicked again |
persist_key |
string | Stable localStorage key for cooldown persistence. Use when label is dynamic (e.g. contains a player name or a variable) — without this, the default key is derived from label+cooldown and breaks if the label changes |
keep_open |
bool | Default: true. Set to false to close the menu on click. |
timeout |
number | 1.0.0 Auto-disable the item after N ms from menu open. Applies to all interactive types (button, slider, toggle, checkbox, list…). |
preview |
table | Hover preview panel — see below |
visible |
bool/fn | Hide the item (supports reactive function) |
disabled |
bool/fn | Disable the item (supports reactive function) |
refresh |
number | Polling interval in ms for reactive visible/disabled |
cb |
function | Called when clicked: function() end |
Preview panel — shown to the right of the menu on hover or keyboard focus:
preview = {
image = "https://...", -- optional image
title = "Engine Repair",
desc = "Restores engine to 100%.",
stats = {
{ label = "Before", value = 20, max = 100 },
{ label = "After", value = 100, max = 100, color = "#4ade80" },
},
}Examples:
-- Simple button
menu:button("Repair Engine", {
icon = "wrench",
cb = function() print("repaired") end,
})
-- Badge + gradient
menu:button("Buy Item", {
icon = "shopping-cart",
color = "#4ade80",
gradient = true,
badge = "500€",
cb = function() end,
})
-- Hold-to-confirm (1.5s)
menu:button("Delete Save", {
icon = "trash",
confirm_hold = true,
cb = function() print("deleted") end,
})
-- Cooldown (5 seconds)
menu:button("Use Ability", {
icon = "zap",
cooldown = 5000,
cb = function() print("ability used") end,
})
-- Auto-disable after 8 seconds (1.0.0)
menu:button("Offre limitée (8s)", {
icon = "clock",
badge = "EXPIRATION",
timeout = 8000,
cb = function() print("accepted in time!") end,
})
-- With hotkey display
menu:button("Interact", {
icon = "mouse-pointer",
hotkey = "E",
cb = function() end,
})
-- Sub-menu arrow (keep_open so parent stays open)
menu:button("More Options", {
icon = "settings",
arrow = true,
keep_open = true,
cb = function()
UI:context(function(sub)
sub:title("More Options")
sub:button("Option A", { cb = function() end })
end)
end,
})Note: The label can be a function for real-time updates — see section 4.
A horizontal slider with min/max/step.
menu:slider("Volume", {
icon = "volume-2",
min = 0,
max = 100,
step = 5,
default = 80,
suffix = "%",
cb = function(value) print("Volume:", value) end,
})Keyboard: ← → to step. Mouse: drag the track.
A − / value / + control for integer values.
menu:stepper("Repair Kits", {
icon = "package",
min = 0,
max = 10,
step = 1,
default = 2,
suffix = " kits",
cb = function(value) print("Kits:", value) end,
})| Option | Type | Description |
|---|---|---|
icon |
string | Lucide icon name |
min |
number | Minimum value (default: 0) |
max |
number | Maximum value (default: 99) |
step |
number | Increment per step (default: 1) |
default |
number | Initial value |
suffix |
string | Unit label appended to the value (e.g. " kits", " m") |
id |
string | Stable callback ID |
cb |
function | Called on change: function(value) end |
Keyboard: ← → to step.
A square checkbox that toggles a boolean.
menu:checkbox("Enable Turbo", {
icon = "zap",
default = false,
cb = function(checked) print("Turbo:", checked) end,
})Keyboard: Enter or Space to toggle.
An animated pill switch (ON/OFF).
menu:toggle("Stealth Mode", {
icon = "eye-off",
default = true,
cb = function(enabled) print("Stealth:", enabled) end,
})Keyboard: Enter or Space to toggle.
A ‹ value › carousel for a list of string options.
menu:list("Fuel Type", {
icon = "fuel",
items = { "Gasoline", "Diesel", "Electric", "Hybrid" },
default = 1, -- 1-based index
cb = function(index, value) print("Fuel:", value) end,
})Keyboard: ← → to cycle.
A read-only progress bar. Default color is the player's accent color (chosen in F12 settings). Pass color to override, or color = "auto" for the legacy green/orange/red percentage-based coloring.
-- New signature (1.0.0) — value and max in opts:
menu:stat("Engine", {
value = 78,
max = 100,
icon = "activity",
suffix = "%",
-- color = "#60a5fa", -- explicit hex override
-- color = "auto", -- green/orange/red based on value/max ratio
})
-- Legacy signature (still supported):
menu:stat("Engine", 78, 100, { icon = "activity", suffix = "%" })value and max both support reactive functions (see section 4):
menu:stat("Health", {
value = function() return GetEntityHealth(PlayerPedId()) - 100 end,
max = 100,
icon = "heart",
suffix = "hp",
refresh = 500,
})A text or number input field embedded in the menu row.
menu:input_inline("Vehicle Name", {
icon = "edit",
type = "text", -- "text" | "number"
placeholder = "My ride...",
default = "",
maxlen = 24,
cb = function(value) print("Name:", value) end,
})
-- Number input with clamped range
menu:input_inline("Speed Limit", {
icon = "gauge",
type = "number",
default = 80,
min = 0,
max = 300,
cb = function(value) print("Speed:", value) end,
})| Option | Type | Description |
|---|---|---|
type |
string | "text" (default) or "number" |
placeholder |
string | Placeholder text |
default |
string|number | Initial value |
maxlen |
integer | Max character length (text) |
min |
number | Minimum value — HTML native constraint, applies to type = "number" |
max |
number | Maximum value — HTML native constraint, applies to type = "number" |
cb |
function | Called on confirm (Enter or blur): function(value) end |
Keyboard: Enter while the row is focused → focuses the input. Enter inside the input → confirms. Escape → blurs.
A color swatch with a preset grid and a custom color input.
menu:color_picker("Body Color", {
icon = "palette",
default = "#e94560",
presets = { "#e94560", "#60a5fa", "#4ade80" }, -- optional custom presets
cb = function(hex) print("Color:", hex) end,
})Keyboard: Enter on the row opens the popover. ← → ↑ ↓ navigate presets. Enter/Space selects. Escape closes.
Three number inputs for day/month/year. Returns an ISO date string ("YYYY-MM-DD").
menu:date_picker("Review Date", {
icon = "calendar",
default = "2025-06-15", -- optional, defaults to today
min = "2024-01-01", -- optional lower bound
max = "2030-12-31", -- optional upper bound
format = "dmy", -- 'dmy' (default) | 'mdy' | 'ymd'
cb = function(date) print("Date:", date) end,
})| Option | Type | Description |
|---|---|---|
icon |
string | Lucide icon name |
default |
string | Pre-filled date in "YYYY-MM-DD" format (defaults to today) |
min |
string | Minimum selectable date ("YYYY-MM-DD") |
max |
string | Maximum selectable date ("YYYY-MM-DD") |
format |
string | Display order for the three inputs: 'dmy' (DD/MM/YYYY, default), 'mdy' (MM/DD/YYYY), 'ymd' (YYYY/MM/DD) |
id |
string | Stable callback ID |
cb |
function | Called on confirm: function(date) end — returns "YYYY-MM-DD" |
A thin horizontal line.
menu:separator()A section label (all-caps, dimmed).
menu:header("Vehicle Stats", { color = "#60a5fa", align = "left" })| Option | Type | Description |
|---|---|---|
color |
string | Text color override (hex) |
align |
string | Text alignment: 'left' (default), 'center', 'right' |
id |
string | Stable item ID |
A collapsible section. Any item type can be placed inside.
Note: All buttons default to
keep_open = true— clicking them does not close the menu. Setkeep_open = falseexplicitly to close on click.
menu:accordion("Player Info", function(acc)
acc:stat("Health", 85, 100, { icon = "heart", suffix = "%" })
acc:stat("Armor", 60, 100, { icon = "shield", suffix = "%" })
acc:button("Heal", {
icon = "plus-circle",
cb = function() print("healed") end,
-- keep_open = false, -- uncomment to close menu on click
})
end, {
icon = "user",
open = true, -- open by default (default: false)
})Keyboard: Enter on the accordion header toggles it.
Organizes items into named tabs. A tab bar is automatically shown at the top of the menu when at least one tab is defined.
menu:tab("Weapons", function(t)
t:button("Pistol", { icon = "crosshair", badge = "500€", cb = function() end })
t:button("Rifle", { icon = "crosshair", badge = "1200€", cb = function() end })
end, { icon = "crosshair" })
menu:tab("Ammo", function(t)
t:button("9mm x50", { icon = "package", badge = "80€", cb = function() end })
t:button("Rifle x30", { icon = "package", badge = "120€", cb = function() end })
end, { icon = "package" })Items placed outside any tab block are always visible (above the tab bar content area).
Pass a function as the label to a button for dynamic text that auto-updates.
Use visible or disabled as functions for items that show/hide or enable/disable based on game state.
local startTime = GetGameTimer()
UI:context(function(menu)
menu:title("Live Info")
-- Dynamic label (updates every 1s)
menu:button(function()
local elapsed = math.floor((GetGameTimer() - startTime) / 1000)
return string.format("Open for: %ds", elapsed)
end, { refresh = 1000 })
-- Conditional visibility (checks every 500ms)
menu:button("Low Health Warning", {
icon = "heart",
color = "#e94560",
visible = function() return GetEntityHealth(PlayerPedId()) < 150 end,
refresh = 500,
})
end)refresh (ms) controls how often the watcher function is called. Default: 500ms for all reactive fields (label, visible, disabled, color, badge). Minimum recommended: 100ms.
Build once, open multiple times without re-running the builder.
local shopMenu = UI:context_build(function(menu)
menu:title("Shop")
menu:button("Buy Item — 50€", {
icon = "shopping-cart",
cb = function() print("purchased") end,
})
menu:button("Close", { icon = "x", cb = function() end })
end)
-- Later, open it:
RegisterCommand('openshop', function()
shopMenu:open()
end, false)Note: callbacks are registered once. Reactive watchers start fresh on each
open().
Open a second context menu from within a callback. The parent stays in the stack.
menu:button("Advanced Options", {
icon = "settings",
arrow = true,
keep_open = true, -- required: keeps parent menu open
cb = function()
UI:context(function(sub)
sub:title("Advanced Options")
sub:animation("slideRight")
sub:button("Option A", { cb = function() end })
sub:button("Back", { icon = "arrow-left", cb = function() end })
end)
end,
})Use the builder shorthand methods to reduce boilerplate when nesting menus:
UI:context(function(menu)
menu:title("Main Menu")
-- b:submenu(label, builderFn, opts) — equivalent to keep_open button that opens a sub-context
menu:submenu("Paramètres", function(sub)
sub:title("Paramètres")
sub:button("Audio", { icon = "volume-2", cb = function() end })
sub:button("Vidéo", { icon = "monitor", cb = function() end })
-- b:back() — closes this sub-menu and returns to the parent
sub:back("Retour", { icon = "arrow-left" })
end, { icon = "settings" })
end)menu:back(label, opts) is equivalent to a button whose callback calls Stack.pop(). Pass opts.cb to run cleanup logic before popping:
sub:back("Retour", {
icon = "arrow-left",
cb = function() cleanup() end -- optional, runs before pop
})Pressing Escape closes the top menu and returns to the parent.
context_update rebuilds the currently visible context menu without closing and reopening it. Use it to refresh item lists that aren't covered by individual watchers (e.g., the list changed entirely — new items added/removed, not just field values).
local UI = exports['LastMenu']
-- Rebuild the top context menu with a new builder function
UI:context_update(function(menu)
menu:title("Shop")
for _, item in ipairs(getUpdatedInventory()) do
menu:button(item.name, { badge = item.price .. "€", cb = function() buy(item) end })
end
end)Only works when the top of the stack is a context menu. If the stack is empty or the top is a different type, the call is silently ignored.
When to use context_update vs reactive watchers:
| Situation | Recommended |
|---|---|
| A button label/badge/color/visible changes | Watcher (visible = function(), badge = function()) |
| The item list itself changes (add/remove items) | context_update |
| Switching "tabs" by reloading the entire menu | context_update |
Handle variant — context_build handles expose the same method scoped to that specific menu (even if it's not on top):
local shopHandle = UI:context_build(function(menu)
-- ...
end)
-- Later, rebuild without closing/reopening:
shopHandle.update(function(menu)
menu:title("Shop (refreshed)")
-- ...
end)
-- Or re-run the original builder:
shopHandle.update()Players can open the user configurator at any time by pressing F12 (default keybind, rebindable in GTA5 controls).
| Setting | Options | Effect |
|---|---|---|
| Navigation mode | Mouse only / Keyboard only / Both | Controls how the menu responds to input |
| Target key | Left Ctrl / E / G / Alt | Key used to open target action menus |
| Accent color | 10 presets + custom picker | Changes --ui-accent globally |
| Menu opacity | Slider | Changes --ui-ctx-bg alpha |
| Menu width | Slider | Changes --ui-ctx-width |
| Compact mode | Toggle | Reduces item height (--ui-ctx-item-height) |
| Blur effect | Toggle | Enables/disables backdrop-filter |
| Font size | 80% to 130% | Changes --ui-font-scale |
| UI sounds | Toggle | Enables/disables click/hover sounds |
| Notification position | 4 quadrants | Toast placement |
| Reset position | Button | Returns menu to default screen position |
Settings are saved to localStorage and persist between sessions. The developer's menu:nav('mouse') / menu:nav('keyboard') always takes priority over the user's global navigation setting.
Open or close the settings panel programmatically:
exports.LastMenu:settings_open() exports.LastMenu:settings_close()
Callbacks are client-side only. LastMenu emits no network events. Every cb function runs in the client Lua context — there is no server-side interaction unless your callback explicitly triggers one (e.g. via TriggerServerEvent). This means:
- No event spoofing risk from LastMenu itself.
- Server-side validation is the developer's responsibility when callbacks trigger server events.
Banner and image URLs are validated by the NUI layer: only https:// and nui:// schemes are accepted. Any other URL is silently discarded before rendering.
CSS injection is prevented on all color and style fields — values are stripped to safe characters before being applied to inline styles.