NUI and CEFbeginner10 min readupdated 19 Sept 2026

Opening and closing NUI with RegisterKeyMapping

The short answer

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.

Opening and closing NUI with RegisterKeyMapping — illustrated with a FiveM-compatible minimap render

#The basic toggle

client.lua
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:

MapperExample keysNotes
keyboardA–Z, 0–9, F1–F24Letters and function keys
keyboardTAB, RETURN, BACK, SPACE, ESCAPEBACK is Backspace, RETURN is Enter
keyboardLMENU, RMENU, LCONTROL, LSHIFTLMENU is left Alt
keyboardUP, DOWN, LEFT, RIGHT, PAGEUP, HOME, ENDNavigation keys
keyboardNUMPAD0–NUMPAD9, ADD, SUBTRACT, MULTIPLYNumpad keys
keyboardGRAVE, COMMA, PERIOD, SLASH, MINUS, EQUALSSymbol keys
MOUSE_BUTTONMOUSE_LEFT, MOUSE_RIGHT, MOUSE_MIDDLE, MOUSE_EXTRABTN1Extra buttons go up to MOUSE_EXTRABTN5
MOUSE_WHEELIOM_WHEEL_UP, IOM_WHEEL_DOWNWheel 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.

client.lua — scoreboard visible while the key is held
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.

client.lua
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)
app.js
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_open rather than phone.
  • 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:

client.lua
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

SymptomCause
Key does nothingThe command is restricted (third RegisterCommand argument true) or registered in a different resource than the mapping
Key opens the UI but cannot close itPage has focus and the game never sees the key; close from the page
Hold UI sticks on screenThe + command took NUI focus, so - never ran
Player’s custom key resetThe command was renamed
New default key not appliedExisting players keep their saved binding
Two actions on one keyAnother resource uses the same default; pick a different one
UI opens over the pause menuMissing 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.

client.lua
RegisterKeyMapping('+radialmenu', 'Radial menu (hold)', 'keyboard', 'LMENU')
RegisterKeyMapping('~!+radialmenu', 'Radial menu (hold) - alternate', 'MOUSE_BUTTON', 'MOUSE_EXTRABTN1')
UI typeToggleHold
Inventory, phone, shopYes: players browse and clickNo
Scoreboard, player listPossibleYes: glance and release
Radial menuPossibleYes: hold, point, release to select
Map or info overlayYesEither; 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.

Share this guideXRedditFacebook

Questions

How do I bind a key to open my NUI in FiveM?
Register a command that opens the UI and call RegisterKeyMapping('command', 'Label', 'keyboard', 'F2').
Where can players change the key?
In Settings → Key Bindings → FiveM. Their choice is saved locally and kept across sessions.
Why can I not close my UI with the same key?
While the page has NUI focus the game does not receive the key. Close from the page on keyup, or use keep input.
How do I bind a mouse button?
Use the 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?
Bindings are saved per player on first use. A new default only applies to players who never had the binding.

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.