fabric-client-rendering
Skill4 starsChanged 12 days agoMITView on GitHub
Draw things on the Minecraft client with Fabric - HUD elements (HudElementRegistry and the older HUD callbacks), GUI drawing (GuiGraphicsExtractor / GuiGraphics / DrawContext depending on version), text and Components, item tooltips, custom Screens and widgets, and render performance. Use when adding or fixing a HUD overlay, screen, tooltip or any per-frame client drawing code.
In this repository10 other files it gives its agents.
- checkSkill
- fabric-config-and-keybindsSkill
- fabric-crash-debuggingSkill
- fabric-mixinsSkill
- fabric-networkingSkill
- fabric-project-setupSkill
- fabric-releaseSkill
- fabric-version-portingSkill
- newSkill
- portSkill
The file
We split this skill into the rules it gives an agent
11 rules. About 0 are common, 2 are distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 8
All code in this skill is client-only: it belongs in src/client/java and is registered from your ClientModInitializer.
- Very specific: line 87
Use DeltaTracker#getGameTimeDeltaPartialTick(false) for tick-interpolated animation and Util.getMillis() for wall-clock animation.
- Very specific: line 123
Use 0xFFFFFFFF, or wrap RGB with ARGB.opaque(rgb).
- Very specific: line 135
Put user-visible strings in assets/<modid>/lang/en_us.json and use Component.translatable.
- Very specific: line 186
Create widgets in init(), not in the constructor.
- Very specific: line 188
Always keep a parent and return to it (Mod Menu and options screens rely on this).
- Very specific: line 191
Prefer vanilla widgets (Button, EditBox, CycleButton, AbstractSliderButton) before writing custom ones.
- Very specific: line 202
No game-state work in render: no world scans, entity searches, raycasts, file I/O, config reads from disk, or registry lookups.
- Distinctive: line 205
Do not log from the render path.
- Distinctive: line 206
Avoid one-off textures uploaded each frame.
- Very specific: line 245
Not checking mc.player == null (crash on the title screen) or ignoring the F1 hide-HUD state.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Fabric client rendering
All code in this skill is client-only: it belongs in src/client/java and is registered
from your ClientModInitializer. Referencing it from common code crashes dedicated servers.
Step 0 - Which drawing API does this version have?
The GUI drawing class and the HUD API were renamed several times. Check before writing code:
grep -E "minecraft_version|yarn_mappings" gradle.properties
grep -n "mappings" build.gradle| Version / mappings | GUI draw class | Screen draw method | Text method | HUD API |
| 26.1+ (Mojang, unobfuscated) | GuiGraphicsExtractor | extractRenderState(graphics, mouseX, mouseY, delta) | graphics.text(font, ...) | HudElementRegistry |
| 1.21.6 - 1.21.11, Mojang mappings | GuiGraphics | render(graphics, mouseX, mouseY, delta) | graphics.drawString(font, ...) | HudElementRegistry |
| 1.21.6 - 1.21.11, Yarn | DrawContext | render(context, mouseX, mouseY, delta) | context.drawText(textRenderer, ...) | HudElementRegistry |
| 1.21.2 - 1.21.5 | as above for your mappings | render(...) | as above | HudLayerRegistrationCallback (HudRenderCallback deprecated) |
| 1.20 - 1.21.1 | as above for your mappings | render(...) | as above | HudRenderCallback |
Then confirm in the actual dependency, because names keep moving (26.2 moved
minecraft.setScreen to minecraft.gui.setScreen, for example):
./gradlew genSources
# open HudElementRegistry / your GUI draw class in the IDE, or read Fabric API's sources jarVersion-matched guides with working code: https://docs.fabricmc.net/develop/rendering/hud (use the version selector at the top of the page).
The examples below use the 26.1+ Mojang names. Translate through the table for older versions; the structure is identical.
HUD elements (HudElementRegistry)
package com.example.examplemod.client;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry;
import net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import com.example.examplemod.ExampleMod;
public class ExampleModClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
HudElementRegistry.attachElementAfter(
VanillaHudElements.HOTBAR, ExampleMod.id("coords"), ExampleModClient::extractCoords);
}
private static void extractCoords(GuiGraphicsExtractor graphics, DeltaTracker delta) {
Minecraft mc = Minecraft.getInstance();
// F1 "hide HUD": mc.gui.hud.isHidden() on 26.3; mc.options.hideGui on older versions.
if (mc.player == null || mc.gui.hud.isHidden() || !ExampleConfig.get().showCoords) {
return;
}
graphics.text(mc.font, CoordsText.current(mc.player), 4, 4, 0xFFFFFFFF, true);
}
}HudElementRegistry methods: addFirst, addLast, attachElementBefore,
attachElementAfter (relative to a VanillaHudElements id such as HOTBAR, CHAT,
CROSSHAIR, BOSS_BAR, SCOREBOARD), removeElement and replaceElement (for hiding or
wrapping a vanilla element). The API handles layering; you do not manage z offsets.
Rules for HUD code:
- Return early when
mc.player == null, when the HUD is hidden with F1 (mc.gui.hud.isHidden()on 26.3,mc.options.hideGuion older versions - Fabric does not hide your element for you), and when your feature is disabled. Also consider hiding while the F3 debug screen is open if your element would overlap it (look up the debug-overlay accessor in your version's sources). - Position relative to
graphics.guiWidth()/graphics.guiHeight()(scaled size), never hardcoded pixel assumptions about the window. - Use
DeltaTracker#getGameTimeDeltaPartialTick(false)for tick-interpolated animation andUtil.getMillis()for wall-clock animation.
Older HUD callbacks (pre-1.21.6)
// 1.20 - 1.21.1 (Mojang names shown; Yarn: DrawContext, RenderTickCounter)
HudRenderCallback.EVENT.register((graphics, deltaTracker) -> { /* draw */ });HudLayerRegistrationCallback (1.21.2 - 1.21.5) registers layers relative to
IdentifiedLayer ids, similar in spirit to HudElementRegistry. Check the docs page for that
exact version before using it.
Drawing primitives
graphics.fill(x1, y1, x2, y2, 0x80000000); // ARGB! 0x80 = 50% alpha
graphics.outline(x, y, width, height, 0xFFFF0000);
graphics.fillGradient(x1, y1, x2, y2, 0xFFFF0000, 0xFF0000FF);
graphics.horizontalLine(x1, x2, y, color);
graphics.verticalLine(x, y1, y2, color);
graphics.enableScissor(x1, y1, x2, y2); // clip; calls must be balanced
// ... draw ...
graphics.disableScissor();
graphics.blit(RenderPipelines.GUI_TEXTURED, ExampleMod.id("textures/gui/icon.png"),
x, y, 0, 0, 16, 16, 16, 16); // pipeline, texture, x, y, u, v, w, h, texW, texH
graphics.blitSprite(RenderPipelines.GUI_TEXTURED, spriteId, x, y, w, h); // GUI atlas sprite
graphics.centeredText(font, component, centerX, y, 0xFFFFFFFF);
graphics.setTooltipForNextFrame(font, component, mouseX, mouseY);
graphics.item(stack, x, y); // item icon (26.x name; older: renderItem / drawItem)Colors are ARGB since 1.21.6. 0xFFFFFF is fully transparent white and renders nothing.
Use 0xFFFFFFFF, or wrap RGB with ARGB.opaque(rgb). This is the most common "my text does
not show up" bug when porting older code.
Text
Component title = Component.translatable("hud.examplemod.title"); // localized
Component value = Component.literal(String.valueOf(count)).withStyle(ChatFormatting.GREEN);
int width = mc.font.width(title);
graphics.text(mc.font, title, x, y, 0xFFFFFFFF, true); // last arg: drop shadow- Put user-visible strings in
assets/<modid>/lang/en_us.jsonand useComponent.translatable. Hardcoded English inComponent.literalcannot be translated. - Yarn equivalents:
Text.translatable,Text.literal,Formatting,textRenderer.
Tooltips
Append lines to any item's tooltip without a mixin:
ItemTooltipCallback.EVENT.register((stack, tooltipContext, tooltipFlag, lines) -> {
if (stack.is(Items.COMPASS)) {
lines.add(Component.translatable("tooltip.examplemod.compass").withStyle(ChatFormatting.GRAY));
}
});ItemTooltipCallback lives in net.fabricmc.fabric.api.client.item.v1. For your own
items, override the item's tooltip method or use a data component instead. Use
tooltipFlag.isAdvanced() to gate debug info behind F3+H.
Custom screens
public class ExampleScreen extends Screen {
private final Screen parent;
public ExampleScreen(Screen parent) {
super(Component.translatable("screen.examplemod.title"));
this.parent = parent;
}
@Override
protected void init() { // called on open AND on every resize - rebuild widgets here
this.addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> this.onClose())
.bounds(this.width / 2 - 100, this.height - 28, 200, 20)
.build());
}
@Override
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) {
super.extractRenderState(graphics, mouseX, mouseY, delta); // background + widgets
graphics.centeredText(this.font, this.title, this.width / 2, 15, 0xFFFFFFFF);
}
@Override
public void onClose() {
this.minecraft.gui.setScreen(this.parent); // 26.2+; earlier: this.minecraft.setScreen(...)
}
}- Create widgets in
init(), not in the constructor;init()reruns on resize and the widget list is cleared each time. - Always keep a
parentand return to it (Mod Menu and options screens rely on this). - Open a screen from a key press or tick handler on the client thread. Opening it from a
network handler or another thread needs
client.execute(() -> ...). - Prefer vanilla widgets (
Button,EditBox,CycleButton,AbstractSliderButton) before writing custom ones.
Performance - this code runs every frame
HUD and screen draw methods run 60-500+ times per second. Treat them as a hot loop:
- No allocations per frame where avoidable: no
newobjects, noString.format, no string concatenation in the draw path, no streams/lambdas created per call, noComponent.literalof a value that has not changed. Cache theComponentand rebuild it only when the underlying value changes (e.g. on a tick event, or when the block position differs). - No game-state work in render: no world scans, entity searches, raycasts, file I/O,
config reads from disk, or registry lookups. Compute in
ClientTickEvents.END_CLIENT_TICK(20 times/s) and draw the cached result. - Do not log from the render path.
- Avoid one-off textures uploaded each frame; register textures once.
- Measure with the F3 frame-time graph or a profiler (Spark) before and after.
// Good: recompute at most once per tick, only if changed
public final class CoordsText {
private static Component cached = Component.empty();
private static BlockPos last = null;
public static void onTick(Minecraft mc) { // from END_CLIENT_TICK
if (mc.player == null) return;
BlockPos pos = mc.player.blockPosition();
if (!pos.equals(last)) {
last = pos;
cached = Component.translatable("hud.examplemod.coords", pos.getX(), pos.getY(), pos.getZ());
}
}
public static Component current(LocalPlayer player) { return cached; }
}World rendering
Drawing in the 3D world (outlines, boxes, lines) uses LevelRenderEvents (26.1+; named
WorldRenderEvents before) and render pipelines, and changes nearly every version as Mojang
rewrites the renderer. Read https://docs.fabricmc.net/develop/rendering/world for your exact
version and copy its structure; do not adapt a world-rendering snippet from another version.
Common mistakes
- Using
DrawContext/MatrixStack/TextRenderer(Yarn) in a Mojang-named project, orGuiGraphicsin a 26.1+ project where it isGuiGraphicsExtractor. - Overriding
render(...)in a 26.1+Screen- the method isextractRenderState; the old name compiles only if you also forget@Override, then never runs. Always use@Override. - RGB colors (
0xFFFFFF) after 1.21.6 -> invisible text and fills. - Using deprecated
HudRenderCallbackon 1.21.6+, or a mixin into the vanillaGuiclass for a simple overlay. - Allocating strings/components every frame, or doing world queries inside the HUD callback.
- Not checking
mc.player == null(crash on the title screen) or ignoring the F1 hide-HUD state. - Building widgets in the
Screenconstructor instead ofinit(). - Forgetting to call
super.extractRenderState(...)/super.render(...)and losing the background and all widgets. - Hardcoded English strings instead of translation keys.
- Placing any of this in
src/main.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

