Mod Deck is a declarative configuration-screen library for Minecraft Java Edition 26.2 and Fabric. Mods register typed options through the public API; Mod Deck renders a consistent settings screen and persists values without requiring each mod to implement Minecraft widgets or file I/O.
This is a non-obfuscated Fabric 26.x project using Mojang's official names and net.fabricmc.fabric-loom. Yarn mappings and remap tasks are intentionally absent. Mod Deck does not depend on Cloth Config, Architectury, Auto Config, or any Cloth implementation classes.
- Minecraft Java Edition 26.2
- Fabric Loader 0.19.3 or newer
- Fabric API 0.155.2+26.2 or newer
- Java 25
ModDeck is published on Modrinth. Add the Modrinth Maven repository and depend on the published artifact. This is a non-obfuscated Fabric 26.2 project, so use the standard Gradle implementation configuration (never modImplementation).
build.gradle (consumer mod):
repositories {
maven { url = 'https://api.modrinth.com/maven' }
}
dependencies {
minecraft "com.mojang:minecraft:${project.minecraft_version}"
implementation "net.fabricmc:fabric-loader:${project.loader_version}"
implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"
implementation 'maven.modrinth:mod-deck:0.1.0'
}fabric.mod.json (consumer mod):
"depends": {
"fabricloader": ">=0.19.3",
"minecraft": "~26.2",
"java": ">=25",
"fabric-api": ">=0.155.2",
"moddeck": "*"
}If you are contributing to ModDeck or need an unreleased snapshot, you can compile against the local source via a Gradle composite build. This is optional and not needed for normal mod development against the published artifact.
settings.gradle (consumer mod):
includeBuild('../ModDeck') {
dependencySubstitution {
substitute module('maven.modrinth:mod-deck') using project(':')
}
}Keep the implementation 'maven.modrinth:mod-deck:0.1.0' line in build.gradle; the composite build substitutes it with the local project so the published and local workflows share one coordinate.
Register a definition from your client mod initializer:
ConfigScreenApi.register(
ConfigDefinition.builder("example_mod")
.titleKey("example_mod.config.title")
.categoryKey("general", "example_mod.category.general")
.booleanOptionKey("enabled", "example_mod.option.enabled",
"example_mod.option.enabled.description", true)
.integerOption("volume",
ConfigText.translatable("example_mod.option.volume"),
ConfigText.translatable("example_mod.option.volume.description"),
50, 0, 100, 1)
.build()
);ConfigText.literal(...) accepts fixed text and ConfigText.translatable(...) accepts a Minecraft translation key. Put translations under the registering mod's own assets/<mod_id>/lang/<language>.json; Mod Deck resolves them at render time and rebuilds widgets when Minecraft reloads the active language.
Built-in entries cover Boolean, Integer, Long, Float, Double, String, Enum, RGB/ARGB Color, Keybind, generic List, generic Selector/Dropdown, Slider, action buttons, description rows, and nested Subcategory values. Entries also support translated/dynamic tooltips, dynamic defaults, per-entry reset, validators, value formatters, search aliases, conditional display/enabling, change callbacks, save consumers, read-only state, and restart-required metadata. SubcategoryOption recursively applies storage, reset, validation, search, and callbacks to its children.
Editing changes draftValue() and invokes onChanged, but the runtime-facing value() remains at the last loaded or saved value. Until Save succeeds, the footer shows an unsaved-change warning. Save first persists every draft, then commits them to value(), and invokes onSaved and the definition's onSave callback. Closing with unsaved edits asks whether to discard them; returning a draft to its last saved value clears the warning automatically.
Keybind entries can independently allow keyboard keys, mouse buttons, and an unbound state:
builder.keybindOption("action", name, description, "key.keyboard.g",
Set.of(KeybindOption.InputType.KEYBOARD, KeybindOption.InputType.MOUSE), true);Call allowModifiers(true) on a KeybindOption to accept Ctrl/Shift/Alt/Super chords. While capturing input, Escape selects the unbound state instead of closing the parent screen. The default keybind shipped by Mod Deck is unbound.
Lists open a dedicated editor with add, remove, reorder, size limits, custom new-element suppliers, and per-element validation. Colors use a compact overlay on the current screen with a hue wheel, saturation/value area, RGB/ARGB channel controls, and an explicit Apply action.
Action buttons run a callback supplied by the registering mod and do not participate in storage or dirty-state tracking:
builder.buttonOption("reload", ConfigText.literal("Reload data"), ConfigText.empty(),
ConfigText.literal("Run"), this::reloadData);Conditional entries use draft values, so dependent controls react while editing:
advanced.displayedWhen(ConfigRequirement.isTrue(enabled));
count.enabledWhen(ConfigRequirement.isValue(mode, Mode.DETAILED));Mods can also register named presets that apply several typed draft values without saving them. Targets use both category and option IDs, so repeated option names in different categories remain unambiguous:
ConfigPreset performance = ConfigPreset.builderKey(
"performance", "example_mod.preset.performance")
.set("video", "particles", ParticleMode.MINIMAL)
.set("video", "render_distance", 8)
.build();
ConfigDefinition definition = ConfigDefinition.builder("example_mod")
.category("video", "Video")
.enumOption("particles", "Particles", ParticleMode.ALL, ParticleMode.class)
.integerOption("render_distance", "Render distance", 16, 2, 32)
.preset(performance)
.build();
definition.applyPreset("performance"); // Draft only; save remains explicit.Unknown targets, duplicate preset IDs, non-persistent entries, incompatible types, and invalid values are rejected with an exception. Registered metadata is available through presets() and preset(id) for custom preset pickers or other integrations.
For annotation-driven registration, annotate a POJO with @ModDeckAutoConfig, mark fields with @AutoEntry, and add @AutoRange, @AutoColor, or @AutoKeybind where appropriate. Then call AutoConfig.register(config). The returned AutoConfigHolder exposes the generated definition and load/save listeners. This is an independent UTF-8 JSON implementation; it does not use Cloth Auto Config or its serializers.
Categories are not predefined. Registration order is the default display order; the category(id, text, order) overload supplies an explicit order. A single category uses no tab bar, while large category sets use a scrollable tab window.
For a custom entry, subclass ConfigOption<T> and register its client widget with OptionWidgetRegistry.register(...). This keeps the storage model independent from Minecraft client classes while allowing a completely custom AbstractWidget.
Duplicate mod IDs, category IDs, and option IDs are rejected instead of silently overwritten.
ConfigStorage is a public backend interface. The built-in JsonConfigStorage writes UTF-8 JSON files atomically to:
config/moddeck/<registered_mod_id>.json
Invalid values in an otherwise readable file are logged and ignored per option, leaving the default value active. Mods may install another backend with ConfigScreenApi.useStorage(...).
Open Mod Deck from a supporting mod or Mod Menu integration. You can also assign the optional Open Mod Deck shortcut under Controls > Mod Deck; the default binding is unbound. Select a registered mod from the Settings hub. The included Example Mod definition exercises the translated built-in entries.
Every definition receives moddeck:config/<mod_id> by default. Client integrations can bypass the hub and preserve their parent screen:
Screen screen = ModDeckApi.createConfigScreen("example_mod", parentScreen);
ModDeckApi.openConfigScreen(ConfigScreenApi.route("example_mod"), parentScreen);This is suitable for a mod's own settings button, Mod Menu, another settings hub, or a client command. Unknown mod IDs and routes fail immediately instead of opening an empty screen.
The hub uses a responsive virtual canvas so Minecraft's automatic GUI scale does not collapse the desktop-style layout. Its native 26.2 GUI rendering includes:
- installed-Mod search in the sidebar and a separate current-Mod settings search
- highlighted search matches and a preset picker when the selected Mod registers presets
- fully mod-defined category tabs, nested subcategories, and a fixed action footer
- automatic, light, and dark themes with purple selection and focus accents
- dedicated switch, numeric slider/field, text/list/color/keybind controls, and selectors
- bundled Noto Sans JP UI typography and Lucide SVG-derived icons
- compact layout scaling without changing the player's global GUI-scale preference
Bundled font and icon licenses are documented in THIRD_PARTY_NOTICES.md and included in the built jar under META-INF/licenses/.
src/main common API, storage, entrypoint (com.yoima.moddeck.api, .storage, .internal)
src/client screen, widgets, mixins, client API (com.yoima.moddeck.client.*)
src/test unit tests
api/ developer API documentation
docs/ design notes (e.g. CLOTH_CONFIG_PARITY.md)
Public API lives under com.yoima.moddeck.api. Minecraft client classes are isolated in the src/client source set so the common entrypoint remains dedicated-server safe.
.\gradlew.bat test build --no-daemon --stacktrace
.\gradlew.bat runClient --no-daemonFull API documentation for mod developers and AI coding agents is available in api/README.md.
The Cloth Config v26.2 feature audit and current parity decisions are documented in docs/CLOTH_CONFIG_PARITY.md.
- Modrinth: https://modrinth.com/mod/mod-deck
- Source: https://github.com/yoima-jp/ModDeck
- Issues: https://github.com/yoima-jp/ModDeck/issues
- License: MIT