Opening and closing NUI with RegisterKeyMapping
Register a command, then call RegisterKeyMapping('command', 'Label', 'keyboard', 'F2'). The player can rebind it under Settings → Key Bindings → FiveM, and the choice is saved on their machine. Commands starting with + fire on key down and the matching - command on key up, which is perfect for hold-to-show UIs. While your page has focus the game does not see the key, so close from the page (Escape, a close button, or the same key sent to the page) or from a Lua loop that reads disabled controls. Keep command names stable: renaming one creates a new binding and players lose their custom key.
Old scripts open menus with a while true do Wait(0) loop and IsControlJustPressed(0, 166). That costs a thread every frame, fights with other resources over the same key and cannot be changed by the player. RegisterKeyMapping replaces all of it: the player sees your action in the FiveM key bindings menu, can move it to any key, and your script runs no loop at all. This guide covers the syntax, the NUI-specific part (closing a UI while the game cannot see the key) and the mistakes that make bindings mysteriously stop working.

#The basic toggle
local open = false
local function setOpen(state)
open = state
SetNuiFocus(state, state)
SendNUIMessage({ action = 'visible', value = state })
end
RegisterCommand('openinventory', function()
if IsPauseMenuActive() or IsNuiFocused() then return end
setOpen(true)
end, false)
RegisterKeyMapping('openinventory', 'Open inventory', 'keyboard', 'TAB')
RegisterNuiCallback('close', function(_, cb)
setOpen(false)
cb({})
end)The four arguments are the command to run (also the binding’s identity), the label players see in the settings menu, the mapper (the input device type) and the default key. The guard at the top stops the UI opening over the pause menu or over another focused interface. The third argument to RegisterCommand (false) means no ACE permission is needed, which is what you want for player keybinds.
#Mappers and key names
Key names come from FiveM’s input mapper parameter reference and are case-insensitive in practice, though the reference lists them in capitals. The ones you will use most:
| Mapper | Example keys | Notes |
|---|---|---|
keyboard | A–Z, 0–9, F1–F24 | Letters and function keys |
keyboard | TAB, RETURN, BACK, SPACE, ESCAPE | BACK is Backspace, RETURN is Enter |
keyboard | LMENU, RMENU, LCONTROL, LSHIFT | LMENU is left Alt |
keyboard | UP, DOWN, LEFT, RIGHT, PAGEUP, HOME, END | Navigation keys |
keyboard | NUMPAD0–NUMPAD9, ADD, SUBTRACT, MULTIPLY | Numpad keys |
keyboard | GRAVE, COMMA, PERIOD, SLASH, MINUS, EQUALS | Symbol keys |
MOUSE_BUTTON | MOUSE_LEFT, MOUSE_RIGHT, MOUSE_MIDDLE, MOUSE_EXTRABTN1 | Extra buttons go up to MOUSE_EXTRABTN5 |
MOUSE_WHEEL | IOM_WHEEL_UP, IOM_WHEEL_DOWN | Wheel steps |
#Hold-to-show with + and - commands
When a command name starts with +, FiveM runs it when the key goes down and runs the command with the same name starting with - when the key comes up. That gives you a hold-to-show scoreboard or a radial menu that closes on release, without any per-frame polling.
RegisterCommand('+scoreboard', function()
SendNUIMessage({ action = 'scoreboard', value = true })
end, false)
RegisterCommand('-scoreboard', function()
SendNUIMessage({ action = 'scoreboard', value = false })
end, false)
RegisterKeyMapping('+scoreboard', 'Show scoreboard (hold)', 'keyboard', 'HOME')Notice there is no SetNuiFocus here. A hold-to-show overlay must not take focus, or the release event goes to the page instead of the game, -scoreboard never runs and the overlay sticks. If the held UI must be clickable (a radial menu), use SetNuiFocus(true, true) with SetNuiFocusKeepInput(true) so the game still sees the release; the details are in SetNuiFocusKeepInput.
#Closing with the same key
Players expect the key that opened a menu to close it. With SetNuiFocus(true, …) and no keep input, the game never sees that key, so your command does not run. There are two reliable ways around it.
Option 1: let the page handle it. Send the bound key to the page when opening and let the page close on that key or Escape. FiveM can report the key a mapping is bound to through GetControlInstructionalButton with the command’s hash and the 0x80000000 flag; it returns a string such as t_F2, where the part after t_ is the key label.
local function boundKey(command)
local button = GetControlInstructionalButton(0, joaat(command) | 0x80000000, true)
return button and button:gsub('^t_', '') or nil
end
RegisterCommand('openinventory', function()
if IsPauseMenuActive() or IsNuiFocused() then return end
SetNuiFocus(true, true)
SendNUIMessage({ action = 'visible', value = true, closeKey = boundKey('openinventory') })
end, false)let closeKey = null;
window.addEventListener('message', (e) => {
if (e.data.action === 'visible' && e.data.value) closeKey = (e.data.closeKey || '').toUpperCase();
});
window.addEventListener('keyup', (e) => {
const key = e.key.toUpperCase();
if (e.key === 'Escape' || (closeKey && key === closeKey)) close();
});Browser key names do not always match FiveM’s (Tab vs TAB match after upper-casing; Backspace vs BACK do not), so keep Escape as a universal fallback and test the keys you ship as defaults. Closing on keyup rather than keydown matters: if you release focus on keydown, the game receives the key-up and sometimes the key itself, and Escape opens the pause menu right after your UI closes.
Option 2: keep input on and read the key in Lua. If your UI already uses keep input, the command fires normally and can toggle. For a UI without keep input you can still detect raw control presses in a loop with IsDisabledControlJustPressed, but that brings back the per-frame loop you were trying to avoid, so option 1 is usually cleaner.
#How bindings are stored (and why defaults do not update)
The first time a player joins with your resource, FiveM creates the binding with your default key. If the player changes it, the choice is saved in their local FiveM configuration (fivem.cfg in the CitizenFX app data folder) and follows them to every server that uses the same resource and command name.
- Changing the default key in your code later does not move players who already have the binding. Only new players get the new default. Announce key changes in Discord instead of expecting them to apply.
- Renaming the command creates a brand-new binding. Players lose their custom key and the old entry lingers in their settings. Choose a stable, namespaced name from day one, for example
myphone_openrather thanphone. - The label is only text. You can improve it at any time without side effects.
Players can also bind manually in F8 with bind keyboard F5 "openinventory", and resource-scoped with rbind. That is handy for testing, but RegisterKeyMapping is what makes the action show up in the settings menu.
#Keeping commands out of the chat suggestions
Every keybind is a command, and the default chat resource lists commands as suggestions. +scoreboard and -scoreboard in the chat autocomplete look messy and invite players to type them. Remove them after registering:
CreateThread(function()
Wait(1000)
TriggerEvent('chat:removeSuggestion', '/+scoreboard')
TriggerEvent('chat:removeSuggestion', '/-scoreboard')
end)The short wait gives the chat resource time to receive the suggestions before you remove them. If your server uses a different chat, check its API for the equivalent event.
#Common mistakes
| Symptom | Cause |
|---|---|
| Key does nothing | The command is restricted (third RegisterCommand argument true) or registered in a different resource than the mapping |
| Key opens the UI but cannot close it | Page has focus and the game never sees the key; close from the page |
| Hold UI sticks on screen | The + command took NUI focus, so - never ran |
| Player’s custom key reset | The command was renamed |
| New default key not applied | Existing players keep their saved binding |
| Two actions on one key | Another resource uses the same default; pick a different one |
| UI opens over the pause menu | Missing IsPauseMenuActive() guard |
For help text that tells players which key to press, show the bound key from GetControlInstructionalButton rather than a hard-coded letter; a loading screen that lists key binds, as in loading screen rules and info, should mention that players can change them in settings.
#Alternate keys and choosing toggle or hold
A mapping can also register a secondary default key. Prefix the command with ~! in a second RegisterKeyMapping call, as shown in FiveM’s native documentation. This is useful when a mouse button and a keyboard key should both work out of the box.
RegisterKeyMapping('+radialmenu', 'Radial menu (hold)', 'keyboard', 'LMENU')
RegisterKeyMapping('~!+radialmenu', 'Radial menu (hold) - alternate', 'MOUSE_BUTTON', 'MOUSE_EXTRABTN1')| UI type | Toggle | Hold |
|---|---|---|
| Inventory, phone, shop | Yes: players browse and click | No |
| Scoreboard, player list | Possible | Yes: glance and release |
| Radial menu | Possible | Yes: hold, point, release to select |
| Map or info overlay | Yes | Either; offer both through two mappings |
When in doubt, ask whether the player needs both hands on the UI. If yes, it is a toggle with focus; if the UI is only a quick look, hold without focus is faster and cannot get stuck.
Questions
How do I bind a key to open my NUI in FiveM?
RegisterKeyMapping('command', 'Label', 'keyboard', 'F2').Where can players change the key?
Why can I not close my UI with the same key?
keyup, or use keep input.How do I bind a mouse button?
MOUSE_BUTTON mapper with a key such as MOUSE_EXTRABTN1, or MOUSE_WHEEL with IOM_WHEEL_UP.I changed the default key but players still have the old one. Why?
Ready to pick a map?
Twelve themes on three base map styles, $8 each, instant download.
Relevant to what you just read
The tool for this
A drag-and-drop loading screen, hosted for you From Free, built in the browser, exported as a drop-in resource.
Look it up while you work
The reference tables this guide leans on, free and without an account.
- All 21,631 object model namesEvery model CreateObject accepts, with its hash and a flag for the LOD and proxy names that spawn invisible.