Key bindings let players assign custom keyboard shortcuts to mod actions through the standard Controls screen. In 1.21.1 the underlying KeyMapping class is a plain Minecraft class shared by both loaders, so the binding definition itself can live in common code. Only the registration and input-polling calls differ between NeoForge and Fabric.
KeyMapping class, its registration, and the input handling all belong in client-only code. If the action triggered by the key needs to affect server state, send a custom packet to the server from within the key handler. The Networking tutorial covers how to set that up.Defining the Key Mapping
Create a ExampleKeyBindings class in a client package inside your common project. Keep the KeyMapping instance as a static field so both loader-specific classes can reference it:
The first argument to KeyMapping is a translation key displayed in the Controls screen. The second is the input type: use InputConstants.Type.KEYSYM for keyboard keys and InputConstants.Type.MOUSE for mouse buttons. The third is the default keycode, pulled from LWJGL's GLFW constants. The fourth is the category translation key, used to group bindings in the Controls list.
consumeClick() removes one pending click and returns true if one was queued. Calling it in a while loop drains all clicks that accumulated since the last tick, which prevents actions from being lost when the frame rate is very high. The mc.screen == null guard prevents the action firing while any screen is open, including the inventory and the pause menu.
EXAMPLE_ACTION.isDown() instead of consumeClick() if you want continuous activation while the key is held (for example, to apply a speed boost every tick). consumeClick() fires once per press regardless of how long the key is held; isDown() returns true on every tick the key remains pressed.NeoForge Registration
In our client event class we created earlier, let's add a new event entry:
Then let's create a second events class to cover event subscription on the game bus to poll the key state each tick. This must be a separate class (or the same class with bus = Bus.GAME) because key mapping registration happens on the mod bus and input polling happens on the game bus:
ClientTickEvent.Post fires after all other client tick processing for the current tick. Using the Post variant ensures that input state has been fully updated before you read it.Fabric Registration
On Fabric, register the key binding and hook the client tick callback in your client mod initialiser:
KeyBindingHelper.registerKeyBinding() is from the Fabric API module fabric-key-binding-api-v1, included in the full Fabric API dependency. It returns the same KeyMapping instance it receives, registered into Fabric's key binding registry. ClientTickEvents.END_CLIENT_TICK fires at the end of each client tick, equivalent to NeoForge's ClientTickEvent.Post.
Handling Input
The onClientTick() method in ExampleKeyBindings is the right place for most key-triggered logic. For actions that affect only the client (opening a client-side screen, toggling a HUD element, playing a local sound), you can do the work directly there. The pattern covers three common cases:
Input events on the client always run on the client thread, so Minecraft.getInstance().player is safe to access without scheduling.
Sending a Packet on Key Press
If the key action needs to affect server state (modifying the world, interacting with a block entity, granting the player an effect), send a C2S packet from the key handler and do the actual work in the packet handler on the server. Using the Common Network library from the Networking tutorial, define the packet in your common network package:
Remember to register this in our PacketRegistration file in the same package! Then send it from the key handler:
Always validate on the server that the action is legal for the player before applying it. Never trust the client to send only valid requests.
Lang Entries
Add two translation entries to our ExampleLangProvider: one for the key binding name and one for the category heading in the Controls screen:
The category key is shared across all key bindings in your mod, so add it once regardless of how many bindings you register. If you define multiple bindings, they all appear grouped under the "Example Mod" heading in the Controls screen.
Testing
Launch the game and press Escape, then go to Options and Controls. Scroll to the "Example Mod" section at the bottom of the list. You should see "Example Action" bound to G. Rebind it to a different key to confirm the Controls screen integration works correctly.
Close the Controls screen, join a world, and press the key. You should see the message "Example action triggered!" appear in chat. Press it while a screen is open (for example, the inventory) to confirm the mc.screen == null guard suppresses the action.
If the binding does not appear in the Controls list, check that the registration call ran during startup. On NeoForge, verify that @EventBusSubscriber is on Bus.MOD (not Bus.GAME) for the registration event. On Fabric, confirm KeyBindingHelper.registerKeyBinding() is called from the client initialiser, not the common initialiser.
You can find the source for this tutorial here:
View Source on GitHubCustom Potion Effects (MultiLoader 1.21+)
Create a MobEffect subclass with custom tick logic and attribute modifiers, register it and a matching Potion in common, and add brewing recipes on both loaders via RegisterBrewingRecipesEvent (NeoForge) and FabricBrewingRecipeRegistryBuilder (Fabric).
Continue →