# Opening and closing NUI with RegisterKeyMapping

> Bind FiveM NUI to rebindable keys with RegisterKeyMapping: syntax, mappers, +/- hold keys, closing while focused, showing the bound key and pitfalls.

Source: https://fam-tools.com/guides/fivem-registerkeymapping-nui · FAM Tools · published 2026-09-19 · updated 2026-09-19 · about 10 min

**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.

## Key points

- `RegisterKeyMapping(command, label, mapper, key)` makes any command a rebindable key.
- Mappers: `keyboard`, `MOUSE_BUTTON`, `MOUSE_WHEEL`, `PAD_DIGITALBUTTON`.
- `+name` runs on press, `-name` on release: ideal for hold-to-show.
- With NUI focus on, the game does not see the key; close from the page.
- Changing the default later does not move players who already have the binding.
- Never rename the command of a released keybind.

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

*client.lua*

```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:

| 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 |

> **Pick defaults that do not collide** — F1–F3 and Tab are claimed by half the resources on a typical roleplay server, and Tab is the weapon wheel in vanilla. Check what your framework already binds before choosing a default; two actions on one key both fire.

## 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*

```lua
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](https://fam-tools.com/guides/fivem-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*

```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*

```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*

```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

| 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](https://fam-tools.com/guides/fivem-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*

```lua
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?

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.

## Related

- https://fam-tools.com/guides/fivem-setnuifocuskeepinput
- https://fam-tools.com/guides/fivem-nui-focus-and-cursor
- https://fam-tools.com/guides/fivem-nui-text-input-keyboard
- https://fam-tools.com/guides/fivem-nui-basics
