Communitygithub.com

vvs1off/fabric-client-rendering

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.

What is fabric-client-rendering?

fabric-client-rendering is a Claude Code agent skill that 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.

Works with~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/vvs1off/fabric-skills/tree/main/skills/fabric-client-rendering

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

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 / mappingsGUI draw classScreen draw methodText methodHUD API
26.1+ (Mojang, unobfuscated)GuiGraphicsExtractorextractRenderState(graphics, mouseX, mouseY, delta)graphics.text(font, ...)HudElementRegistry
1.21.6 - 1.21.11, Mojang mappingsGuiGraphicsrender(graphics, mouseX, mouseY, delta)graphics.drawString(font, ...)HudElementRegistry
1.21.6 - 1.21.11, YarnDrawContextrender(context, mouseX, mouseY, delta)context.drawText(textRenderer, ...)HudElementRegistry
1.21.2 - 1.21.5as above for your mappingsrender(...)as aboveHudLayerRegistrationCallback (HudRenderCallback deprecated)
1.20 - 1.21.1as above for your mappingsrender(...)as aboveHudRenderCallback

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 jar

Version-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.hideGui on 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 and Util.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.json and use Component.translatable. Hardcoded English in Component.literal cannot 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 parent and 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 new objects, no String.format, no string concatenation in the draw path, no streams/lambdas created per call, no Component.literal of a value that has not changed. Cache the Component and 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, or GuiGraphics in a 26.1+ project where it is GuiGraphicsExtractor.
  • Overriding render(...) in a 26.1+ Screen - the method is extractRenderState; 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 HudRenderCallback on 1.21.6+, or a mixin into the vanilla Gui class 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 Screen constructor instead of init().
  • 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.

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Related Skills