# Key bindings

> Let players pick a key with the Keybind widget, store it as text with Key.name, and compare it with the game's input on old and new versions.

URL: https://xichil.dev/docs/xis-mvui/v1/guides/keybinds/ · Updated: 2026-11-06 · Applies to: Fabric, Quilt, Legacy Fabric, Forge, NeoForge, MC 1.7.10 - 26.3

The [Keybind](https://xichil.dev/docs/xis-mvui/v1/widgets/keybind/index.md) widget lets the player pick a key. The key is a
[Key](https://xichil.dev/javadoc/xis-mvui/v1/dev/xichil/mvui/Key.html), which is the same object on every Minecraft version, so your config and
menu stay version-independent.

## Edit a key in the menu

```java
Keybind.builder("zoomKey")
        .label(Text.translatable("mymod.zoomKey", "Zoom"))
        .value(() -> config.zoomKey)          // null or Key.UNBOUND show as unbound
        .onChange(key -> config.zoomKey = key)
        .build();
```

The player clicks the button, then presses a key or mouse button. **Escape unbinds** (your `onChange` receives
`Key.UNBOUND`). Call `.allowMouse(false)` to accept keyboard keys only.

> **Note:** The widget only edits a key. It doesn't register a vanilla key mapping and doesn't listen for the key in game.
> There are no modifier combinations such as Ctrl + key.

## Make keys

| Call | Gives |
| --- | --- |
| `Key.keyboard(glfwCode)` | a keyboard key by its GLFW key code, e.g. `Key.keyboard(82)` for R, `Key.keyboard(340)` for Left Shift |
| `Key.mouse(button)` | a mouse button: 0 left, 1 right, 2 middle, 3 and up the side buttons |
| `Key.UNBOUND` | no key |
| `Key.ESCAPE` | the Escape key |

`Key` is immutable; compare keys with `equals`. `isUnbound()`, `type()` (`KEYBOARD` or `MOUSE`) and `code()`
describe a key, and `englishName()` gives "R", "Left Shift" or "Not Bound" for logs and tests (the widget itself
shows vanilla's translated name).

## Store a key

`name()` is the text 1.13+ writes to `options.txt`, and `Key.fromName` reads it back:

```java
String stored = config.zoomKey.name();     // "key.keyboard.z"
Key loaded = Key.fromName(stored);         // null if the text is no key name

config.zoomKey = loaded != null ? loaded : Key.keyboard(90);   // default: Z
```

The same text works on every version, including 1.7.10 – 1.12.2, so one config file format fits all.

## Reset to a default

Keep the built `Keybind` and call `set`, which passes the key to your `onChange`:

```java
Keybind zoom = Keybind.builder("zoomKey")
        .label(Text.translatable("mymod.zoomKey", "Zoom"))
        .value(() -> config.zoomKey)
        .onChange(key -> config.zoomKey = key)
        .build();

Button reset = Button.builder("zoomKey.reset")
        .label(Text.translatable("mymod.zoomKey.reset", "Reset"))
        .onPress(() -> zoom.set(Key.keyboard(90)))
        .build();
```

## Check the key in game

Listening for input is version-specific, so it belongs in your game layer
([Build one mod for every loader](https://xichil.dev/docs/xis-mvui/v1/guides/multiversion-mod/index.md#5-keep-minecraft-out-of-the-shared-code)). Compare
the key there with what that version's input gives you:

| Minecraft | Input codes | Compare with |
| --- | --- | --- |
| 1.13 – 26.3 | GLFW key codes; mouse buttons by number | `key.code()` and `key.type()` |
| 1.7.10 – 1.12.2 | LWJGL 2 key codes; mouse button *b* as `-100 + b` | `key.toLwjgl2()` |

`Key.fromLwjgl2(code)` converts the other way. A few keys exist on only one side: F20 – F25 and the World 1/2
keys can't be bound on 1.7.10 – 1.12.2, and LWJGL 2 keys with no GLFW equivalent become `Key.UNBOUND`.

## Test it

`FakeHost.pressKey(id, key)` clicks the button and presses the key:

```java
host.pressKey("zoomKey", Key.keyboard(82));
assertTrue(host.render().contains("[keybind zoomKey] Zoom: R"));
host.pressKey("zoomKey", Key.ESCAPE);
assertEquals(Key.UNBOUND, config.zoomKey);
```

## Next steps

- [Keybind](https://xichil.dev/docs/xis-mvui/v1/widgets/keybind/index.md): every builder option.
- [Open your menu](https://xichil.dev/docs/xis-mvui/v1/guides/opening-menus/index.md): open the menu from your own key.
