Skip to content
Back to skills

Fabric Config And Keybinds

ASecurity

Add key bindings (KeyMappingHelper / KeyBindingHelper) and a configuration system to a Fabric mod - a dependency-free JSON config with Gson, or Cloth Config / YACL screens - plus optional Mod Menu integration. Use when a mod needs a toggle key, user settings, a config screen, or a "Configure" button in Mod Menu, or when key presses fire twice / not at all.

  • 4 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
ai-agentsrustgojavareactterraformgitapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned November 7, 2026

npx -y skills add vvs1off/fabric-skills --skill fabric-config-and-keybinds --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Fabric Config And Keybinds?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Fabric Config And Keybinds
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/vvs1off-fabric-config-and-keybinds/badge)](https://www.skillsdirectory.com/skills/vvs1off-fabric-config-and-keybinds)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: fabric-config-and-keybinds
description: Add key bindings (KeyMappingHelper / KeyBindingHelper) and a configuration system to a Fabric mod - a dependency-free JSON config with Gson, or Cloth Config / YACL screens - plus optional Mod Menu integration. Use when a mod needs a toggle key, user settings, a config screen, or a "Configure" button in Mod Menu, or when key presses fire twice / not at all.
---

# Fabric config and key bindings

Key bindings and config screens are **client-side**: put them in `src/client` and register
them from the `ClientModInitializer`. The config *data* class can live in `src/main` if the
server also reads it.

## Step 0 - Names for your version

| | 26.3+ (SDL input) | 26.1 - 26.2 | 1.21.x Mojang mappings | Yarn (<= 1.21.11) |
|---|---|---|---|---|
| Helper | `KeyMappingHelper.registerKeyMapping` (`...client.keymapping.v1`) | same as 26.3 | `KeyBindingHelper.registerKeyBinding` (`...client.keybinding.v1`) | `KeyBindingHelper.registerKeyBinding` |
| Vanilla class | `KeyMapping` | `KeyMapping` | `KeyMapping` | `KeyBinding` |
| "was pressed" | `consumeClick()` | `consumeClick()` | `consumeClick()` | `wasPressed()` |
| Input type | `InputConstants.Type.KEYBOARD` | `InputConstants.Type.KEYSYM` | `InputConstants.Type.KEYSYM` | `InputUtil.Type.KEYSYM` |
| Key codes | `InputConstants.KEY_H` (**SDL scancodes**; raw: `org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_H`) | `InputConstants.KEY_H` (GLFW key codes) | `InputConstants.KEY_H` / `GLFW.GLFW_KEY_H` | `GLFW.GLFW_KEY_H` |
| Category | `KeyMapping.Category.register(id)` (object) | same | object since 1.21.9, a translation-key **string** before | same split as Mojang |

**Minecraft 26.3 replaced GLFW with SDL for input.** `InputConstants.Type.KEYSYM` no longer
exists, key codes are SDL scancodes (numerically different from GLFW codes), and any
`org.lwjgl.glfw.GLFW` usage stops compiling. Always use the `InputConstants.KEY_*` constants
instead of raw numbers or GLFW constants so the code survives these changes.

Confirm in the decompiled sources for your version before writing code (`InputConstants`,
`KeyMapping` constructors). Docs: https://docs.fabricmc.net/develop/key-mappings

## Key bindings (26.3 shown)

```java
package com.example.examplemod.client;

import com.mojang.blaze3d.platform.InputConstants;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
import net.minecraft.client.KeyMapping;
import com.example.examplemod.ExampleMod;

public class ExampleModClient implements ClientModInitializer {
    private static final KeyMapping.Category CATEGORY =
        KeyMapping.Category.register(ExampleMod.id("main"));

    private static KeyMapping toggleHud;

    @Override
    public void onInitializeClient() {
        toggleHud = KeyMappingHelper.registerKeyMapping(new KeyMapping(
            "key.examplemod.toggle_hud",       // translation key
            InputConstants.Type.KEYBOARD,      // KEYBOARD or MOUSE (26.1 - 26.2 and older: KEYSYM)
            InputConstants.KEY_H,              // default key; use InputConstants.UNKNOWN.getValue() for "unbound"
            CATEGORY
        ));

        ClientTickEvents.END_CLIENT_TICK.register(client -> {
            while (toggleHud.consumeClick()) {          // while, not if: drains queued presses
                ExampleConfig cfg = ExampleConfig.get();
                cfg.hudEnabled = !cfg.hudEnabled;
                ExampleConfig.save();
            }
        });
    }
}
```

`assets/examplemod/lang/en_us.json`:

```json
{
  "key.category.examplemod.main": "Example Mod",
  "key.examplemod.toggle_hud": "Toggle HUD"
}
```

(Category translation key format for the object-style category: `key.category.<namespace>.<path>`.)

Pre-1.21.9 category, for reference: pass a string such as `"category.examplemod.main"` as the
last constructor argument and translate that string.

Key binding rules:

- Register once, in `onInitializeClient`, and keep the returned instance.
- Poll with `consumeClick()` in a `while` loop inside `ClientTickEvents.END_CLIENT_TICK`. Use
  `isDown()` only for "held" behaviour (e.g. zoom while held).
- Key presses are not delivered to `consumeClick` while a screen is open. To react inside a
  screen, use `ScreenKeyboardEvents` (see the docs page) or the screen's own key handler.
- Choose defaults that do not clash with vanilla or very popular mods; unbound by default is
  fine for niche actions. Users rebind in Options -> Controls.
- Never read raw GLFW key state for game actions; it ignores user rebinding.

## Config, option A: plain JSON with Gson (no dependencies)

Gson ships with Minecraft, so this needs nothing extra. Good default for small mods.

```java
package com.example.examplemod.config;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import net.fabricmc.loader.api.FabricLoader;
import com.example.examplemod.ExampleMod;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

public final class ExampleConfig {
    private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
    private static final Path PATH = FabricLoader.getInstance().getConfigDir().resolve(ExampleMod.MOD_ID + ".json");
    private static ExampleConfig instance = new ExampleConfig();

    // Fields with defaults. Bump CONFIG_VERSION and migrate when you rename/remove fields.
    public int configVersion = 1;
    public boolean hudEnabled = true;
    public int hudX = 4;
    public int hudY = 4;

    public static ExampleConfig get() { return instance; }

    public static void load() {
        if (Files.notExists(PATH)) { save(); return; }
        try (Reader reader = Files.newBufferedReader(PATH)) {
            ExampleConfig loaded = GSON.fromJson(reader, ExampleConfig.class);
            instance = loaded != null ? loaded.validated() : new ExampleConfig();
        } catch (IOException | JsonParseException e) {
            ExampleMod.LOGGER.error("Invalid config {}, using defaults", PATH, e);
            instance = new ExampleConfig();   // do NOT overwrite the user's broken file silently
        }
    }

    public static void save() {
        try {
            Files.createDirectories(PATH.getParent());
            Path tmp = PATH.resolveSibling(PATH.getFileName() + ".tmp");
            try (Writer writer = Files.newBufferedWriter(tmp)) {
                GSON.toJson(instance, writer);
            }
            try {
                Files.move(tmp, PATH, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
            } catch (AtomicMoveNotSupportedException e) {   // some network/synced drives
                Files.move(tmp, PATH, StandardCopyOption.REPLACE_EXISTING);
            }
        } catch (IOException e) {
            ExampleMod.LOGGER.error("Could not save config {}", PATH, e);
        }
    }

    private ExampleConfig validated() {
        hudX = Math.max(0, hudX);
        hudY = Math.max(0, hudY);
        return this;
    }
}
```

Call `ExampleConfig.load()` once from the initializer. Read values through `get()` (in memory);
never read the file from render or tick code.

## Config, option B: a config library with a ready-made screen

| Library | Good for | Where |
|---|---|---|
| **YACL** (Yet Another Config Lib) | Modern, nice screens, built-in serialization | https://modrinth.com/mod/yacl - docs https://docs.isxander.dev/yet-another-config-lib |
| **Cloth Config** | Long-established, many mods use it | https://modrinth.com/mod/cloth-config - docs https://shedaniel.gitbook.io/cloth-config |

Before adding one:

1. Check on Modrinth that a build exists **for your exact Minecraft version**. Libraries lag
   behind new Minecraft releases; if none exists yet, use option A and add a screen later.
2. Copy the Maven repository and coordinates from the library's own docs (do not guess the
   group id or version).
3. Declare it with the right configuration for your Loom flavour and add it to
   `fabric.mod.json` `depends` (it becomes a hard requirement for players).

```groovy
// 26.1+ (non-remapping Loom)
repositories { maven { url = "<library maven from its docs>" } }
dependencies { implementation "<group>:<artifact>:<version>" }

// <= 1.21.11 (remapping Loom)
dependencies { modImplementation "<group>:<artifact>:<version>" }
```

Generate the screen from the library and return it as a `Screen` with a parent - that is all
Mod Menu needs.

## Mod Menu integration (optional, soft dependency)

Mod Menu shows a "Configure" button if you provide a `modmenu` entrypoint.

`build.gradle` - compile against it, run with it in dev, but do not require it:

```groovy
repositories { maven { name = "Terraformers"; url = "https://maven.terraformersmc.com/releases/" } }
dependencies {
    // 26.1+:
    compileOnly "com.terraformersmc:modmenu:${project.modmenu_version}"
    localRuntime "com.terraformersmc:modmenu:${project.modmenu_version}"
    // <= 1.21.11 use modCompileOnly / modLocalRuntime instead
}
```

Look up `modmenu_version` for your Minecraft version on https://modrinth.com/mod/modmenu/versions.

```java
package com.example.examplemod.client.compat;

import com.terraformersmc.modmenu.api.ConfigScreenFactory;
import com.terraformersmc.modmenu.api.ModMenuApi;

public class ModMenuIntegration implements ModMenuApi {
    @Override
    public ConfigScreenFactory<?> getModConfigScreenFactory() {
        return parent -> new ExampleConfigScreen(parent);
    }
}
```

`fabric.mod.json`:

```json
"entrypoints": {
  "main": ["com.example.examplemod.ExampleMod"],
  "client": ["com.example.examplemod.client.ExampleModClient"],
  "modmenu": ["com.example.examplemod.client.compat.ModMenuIntegration"]
},
"suggests": { "modmenu": "*" }
```

The `modmenu` entrypoint is only instantiated when Mod Menu is installed, so the soft
dependency is safe as long as **no other class** references Mod Menu types.

A hand-written `ExampleConfigScreen` is a normal `Screen` (see the `fabric-client-rendering`
skill) with widgets such as `CycleButton.onOffBuilder(...)` that write to the config and
call `ExampleConfig.save()` in `onClose()`.

## Syncing config with a server

Client-only preferences (HUD position, colours) stay on the client. Gameplay rules that the
server enforces must be read **on the server** from the server's config; if the client needs
to know them, send them with a payload (see `fabric-networking`). Never let a client config
value decide a gameplay outcome on the server.

## Common mistakes

- Using `KeyBindingHelper`/`KeyBinding`/`wasPressed()` on 26.1+, or `KeyMappingHelper` on
  1.21.x.
- `if (key.consumeClick())` instead of `while` - presses get lost or delayed.
- Registering the key mapping inside the tick event or re-registering on every world join.
- Passing a string category on 1.21.9+ or a `Category` object on older versions.
- Forgetting the translation keys, so Controls shows `key.examplemod.toggle_hud`.
- Reading/writing the config file every frame or tick.
- Writing the config non-atomically, or overwriting a user's malformed config with defaults
  without telling them.
- Hard `depends` on Mod Menu, or referencing Mod Menu classes outside the `modmenu`
  entrypoint class (crash when it is absent).
- Using `modImplementation` with non-remapping Loom (26.1+) or `implementation` for mods with
  remapping Loom.
- Adding a config library that has no build for the target Minecraft version.
- Trusting client-side config on the server.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.