MultiLoader 1.21+ · Part 24

Key Bindings (MultiLoader 1.21+)

BEGINNER MULTILOADER 1.21-1.21.1 15 min read · Oct 19, 2026
MultiLoader 1.21+ · 28 parts
1 Getting Started with MultiLoader 1.21+ 2 Setting Up RegistrationUtils 3 Creating Items (MultiLoader 1.21+) 4 Creating Blocks (MultiLoader 1.21+) 5 Data Generation: Block & Item Models (MultiLoader 1.21+) 6 Data Generation: Block Loot Tables (MultiLoader 1.21+) 7 Data Generation: Crafting Recipes (MultiLoader 1.21+) 8 Data Generation: Block & Item Tags (MultiLoader 1.21+) 9 Custom Food Items (MultiLoader 1.21+) 10 Custom Tools (MultiLoader 1.21+) 11 Custom Armour (MultiLoader 1.21+) 12 Block Entities (MultiLoader 1.21+) 13 Config Files (MultiLoader 1.21+) 14 Custom Sounds (MultiLoader 1.21+) 15 Events and Listeners (MultiLoader 1.21+) 16 Networking and Custom Packets (MultiLoader 1.21+) 17 Data Generation: Advancements (MultiLoader 1.21+) 18 Data Generation: Language Files (MultiLoader 1.21+) 19 Custom Entities (MultiLoader 1.21+) 20 Ore Generation (MultiLoader 1.21+) 21 Introduction to Mixins (MultiLoader 1.21+) 22 Custom Particles (MultiLoader 1.21+) 23 Menus & Screens (MultiLoader 1.21+) 24 Key Bindings (MultiLoader 1.21+) 25 Custom Potion Effects (MultiLoader 1.21+) 26 Custom Enchantments (MultiLoader 1.21+) 27 Custom Commands (MultiLoader 1.21+) 28 Custom Biomes (MultiLoader 1.21+)

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.

NOTE
Key bindings are client-side only. The 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:

java
public class ExampleKeyBindings {
public static KeyMapping EXAMPLE_ACTION;
public static KeyMapping createExampleAction() {
return new KeyMapping(
"key.examplemod.example_action", // translation key
InputConstants.Type.KEYSYM, // keyboard (not mouse)
GLFW.GLFW_KEY_G, // default key: G
"key.categories.examplemod" // category translation key
);
}
public static void onClientTick() {
if (EXAMPLE_ACTION == null) return;
while (EXAMPLE_ACTION.consumeClick()) {
Minecraft mc = Minecraft.getInstance();
if (mc.player != null && mc.screen == null) {
mc.player.sendSystemMessage(
Component.literal("Example action triggered!"));
}
}
}
}

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.

TIP
Use 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:

java
@SubscribeEvent
public static void registerKeyMappings(RegisterKeyMappingsEvent event) {
ExampleKeyBindings.EXAMPLE_ACTION = ExampleKeyBindings.createExampleAction();
event.register(ExampleKeyBindings.EXAMPLE_ACTION);
}

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:

java
@EventBusSubscriber(modid = Constants.MOD_ID, bus = EventBusSubscriber.Bus.GAME, value = Dist.CLIENT)
public class ClientGameEvents {
@SubscribeEvent
public static void onClientTick(ClientTickEvent.Post event) {
ExampleKeyBindings.onClientTick();
}
}
NOTE
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:

java
public class ExampleModClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
// Our previous entries
ExampleKeyBindings.EXAMPLE_ACTION =
KeyBindingHelper.registerKeyBinding(ExampleKeyBindings.createExampleAction());
ClientTickEvents.END_CLIENT_TICK.register(client ->
ExampleKeyBindings.onClientTick());
}
}

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:

java
public static void onClientTick() {
if (EXAMPLE_ACTION == null) return;
while (EXAMPLE_ACTION.consumeClick()) {
Minecraft mc = Minecraft.getInstance();
if (mc.player == null || mc.screen != null) continue;
// Case 1: Client-only action (display a message)
mc.player.sendSystemMessage(Component.literal("Key pressed!"));
// Case 2: Open a client-side screen
// mc.setScreen(new MyCustomScreen());
// Case 3: Send a C2S packet to trigger something on the server
// Dispatcher.sendToServer(new ExampleC2SPacket());
}
}

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:

java
public class ExampleActionC2SPacket {
public static final ResourceLocation CHANNEL =
ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "example_action_c2s");
public static final StreamCodec<FriendlyByteBuf, ExampleActionC2SPacket> STREAM_CODEC =
StreamCodec.ofMember((p, buf) -> {}, buf -> new ExampleActionC2SPacket());
public static CustomPacketPayload.Type<CustomPacketPayload> type() {
return new CustomPacketPayload.Type<>(CHANNEL);
}
public static void handle(PacketContext<ExampleActionC2SPacket> ctx) {
if (Side.SERVER.equals(ctx.side()) && ctx.sender() != null) {
ServerPlayer player = ctx.sender();
// Do server-side work here
player.server.execute(() -> {
player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SPEED, 200, 0));
});
}
}
}

Remember to register this in our PacketRegistration file in the same package! Then send it from the key handler:

java
while (EXAMPLE_ACTION.consumeClick()) {
if (Minecraft.getInstance().player != null) {
Dispatcher.sendToServer(new ExampleActionC2SPacket());
}
}

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:

java
add("key.examplemod.example_action", "Example Action");
add("key.categories.examplemod", "Example Mod");

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 GitHub
NEXT IN SERIES

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