JelloUI public API documentation for Minecraft Fabric addon developers.
A module is the leaf unit the ClickGUI lists under a category. You describe a
module with ModuleDefinition, JelloUI owns the live state.
Build a definition through ModuleDefinition.Builder:
ModuleDefinition def = ModuleDefinition.builder(owner, JelloId.of("example", "auto_sprint"))
.displayName("Auto Sprint")
.description("Sprints automatically.")
.category(JelloId.of("example", "movement"))
.settings(List.of(settingA, settingB))
.lifecycle(new MyLifecycle())
.defaultEnabled(false)
.build();
api.modules().register(def);
Keys of the definition:
owner and id (constructor) - attribution and stable identitydisplayName - the ClickGUI label; presentation onlydescription - tooltip textcategory - a JelloId of a registered category, or a CategoryDefinitionicon - optional IconDescriptor; retained as metadata but not drawn by
the current ClickGUI bridgesettings - ordered list of top-level SettingDefinitions; duplicate
top-level setting ids are rejected at build timelifecycle - ModuleLifecycle callbacks; defaults to ModuleLifecycle.NONEdefaultEnabled - false by defaultshowInActiveMods and aliases are retained in the public definition as
metadata, but the current ClickGUI/profile bridges do not consume them. Do
not rely on either field for visible behavior or migration.
A definition is immutable once registered. To change it, unregister and register a new definition.
JelloUI owns the live state. After registering, ask the module registry:
ModuleState state = api.modules().state(id).orElseThrow();
ModuleState gives you the enabled flag, the bound keybind and typed access
to every setting handle:
if (state.isEnabled()) { /* ... */ }
state.setEnabled(false);
state.toggle();
Optional<JelloId> bound = state.keybindId();
state.bindKeybind(JelloId.of("example", "sprint_key"));
Optional<SettingHandle<Double>> speed = state.setting(id, Double.class);
Collection<SettingHandle<?>> all = state.settings();
The Class<T> overload is a compile-time convenience only; the current
implementation does not validate the handle’s runtime value type. Keep
setting ids and expected types paired in constants and definitions.
ModuleState is thread-confined to the client thread. Mutating it from
another thread is not supported.
ModuleLifecycle is an interface with default no-op implementations:
onInit() - once, after registration, before first toggleonEnable() / onDisable() - when the enabled flag changesonTick() - client tick while enabledonShutdown() - once, when the module is unregisteredEvery callback runs on the client thread behind JelloUI’s error isolation
guard. A failure is reported and swallowed so it cannot abort another
module, tick or shutdown pass. A throwing onEnable() callback does not roll
back the enabled flag; if partial initialization is unsafe, make the callback
transactional or explicitly disable the state after your own cleanup.
For a module that only groups settings or is toggled by other modules, pass
ModuleLifecycle.NONE instead of an empty anonymous class.
Settings are typed value definitions. JelloUI generates the matching widget for each one automatically. The value you store is always the stable form; the renderer is presentation.
new BooleanSetting(id, "Enabled", "Master switch.", true);
NumberSetting.builder(id, "Speed")
.min(0).max(10).step(0.1).defaultValue(2.5)
.unit("bps").decimals(1)
.build();
NumberSetting stores a Double. For integer-only sliders, keep the step
and bounds to integers and cast at read time.
The current ClickGUI bridge applies min, max and step. It does not
consume unit, decimals or logarithmic; slider formatting is derived from
the legacy slider’s step instead.
ModeSetting.builder(id, "Mode")
.mode("auto", "Automatic")
.mode("manual", "Manual")
.defaultValue("auto")
.build();
The stored value is the stable key, never the label. Renaming a label does not orphan stored values. The default must be one of the declared modes. The current ClickGUI bridge displays those stable keys and does not consume the separate human-readable labels.
TextSetting.builder(id, "Label")
.defaultValue("Hello")
.maxLength(64)
.placeholder("Type a label")
.password(false)
.build();
maxLength and an optional regex validation are enforced when an addon
writes through SettingHandle.set. The current legacy text field does not
consume maxLength, validation, placeholder or password: it does not
mask password text and UI edits can bypass those API-side constraints.
new ColorSetting(id, "Color", "ARGB color.", 0xFF3D7AFF,
true /* rainbow */, true /* alpha */, false /* defaultRainbow */, null);
The stored value is an ARGB int. Rainbow derives the rendered hue from the
system clock, matching the Sigma implementation. defaultRainbow is applied
by the current bridge. rainbowSupported and alphaSupported are retained as
metadata but are not used to configure the legacy picker, which always offers
its rainbow toggle and has no API-controlled alpha mode.
GroupSetting.of(id, "Advanced", "Collapsible.", true, List.of(childSetting));
A collapsible group whose children keep their own ids; the group does not
prefix them. Child-id uniqueness is the addon’s responsibility and is not
checked against top-level or sibling-group ids. The current ModuleState
exposes the group handle but not its child handles, and child visibility
predicates are not wired by the legacy bridge. Avoid groups for values that
addon code must read or mutate through the public state API.
new ActionSetting(id, "Reset", "Resets the module.", this::reset,
null /* icon */, true /* destructive */, null);
An action setting has no stored value; pressing it runs the Runnable behind
the error isolation guard. The destructive flag changes the ClickGUI
accent so it reads as a destructive action. The optional action icon is
currently retained as metadata and is not drawn.
new CustomSetting<>(id, "Rating", "Rendered by the addon.", defaultValue,
new MyRenderer(), null);
Custom settings let an addon draw a widget JelloUI has no built-in renderer
for. The renderer receives a rectangle and a live handle, and failures are
isolated. The current bridge forwards drawing but no generic mouse or keyboard
input. The value is opaque and is not automatically serialized; persist it
with your own ProfileSectionCodec when needed.
Every setting can carry a visibility predicate:
Predicate<SettingVisibilityContext> show =
ctx -> Boolean.TRUE.equals(ctx.valueOf(masterToggleId, Boolean.class));
new BooleanSetting(id, "Conditional", "Shown when the toggle is on.", false, show);
SettingVisibilityContext resolves another setting’s current value by id.
The context is re-evaluated every frame the panel is open, so visibility
tracks live changes. An unknown id or a type mismatch returns null; write
predicates defensively as in the example above.
You read and write a setting through SettingHandle<T>:
SettingHandle<Double> handle = state.setting(id, Double.class).orElseThrow();
double speed = handle.get();
handle.set(3.0);
handle.reset();
handle.isDefault();
AutoCloseable sub = handle.subscribe(value -> { /* on the client thread */ });
set validates through the definition. Invalid values are rejected with
ApiUsageException. Subscribers are invoked on the client thread. The handle
is thread-confined like the state.
If your module would rather keep its own typed state view than read handles
every frame, implement a StateAdapter<S>:
StateAdapter<MySnapshot> adapter = state -> Optional.of(new MySnapshot(
state.isEnabled(),
StateAdapter.value(state.setting(speedId, Double.class), 0.0)));
StateAdapter.value(handle, fallback) is a helper that unwraps an
Optional<SettingHandle<T>> with a fallback. StateAdapter is not registered
with or scheduled by the current runtime: your addon calls snapshot(state)
when it wants a new view.
You can bind a keybind to a module so the module toggles when the key fires:
state.bindKeybind(JelloId.of("example", "sprint_key"));
// pass null to clear:
state.bindKeybind(null);
The keybind must already be registered; an unknown id logs a warning and
leaves the existing module key unchanged. See
04-surfaces. The current bridge copies the
keybind’s raw key code into the legacy module when bindKeybind is called. A
later KeybindRegistry.setBinding does not automatically recopy it, and
keybindId() exposes a synthetic module-derived id rather than the original
registered id. Treat the registry binding as authoritative and call
bindKeybind again after rebinding when module-key synchronization matters.