Skip to content
Back to skills

Fabric Client Rendering

ASecurity

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.

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

Works with

  • cli
  • api

Security analysis

A100/100

Scanned November 7, 2026

npx -y skills add vvs1off/fabric-skills --skill fabric-client-rendering --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Fabric Client Rendering?

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

Security grade badge for Fabric Client Rendering
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/vvs1off-fabric-client-rendering/badge)](https://www.skillsdirectory.com/skills/vvs1off-fabric-client-rendering)

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-client-rendering
description: 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.
---

# 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:

```bash
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):

```bash
./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)

```java
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)

```java
// 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

```java
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

```java
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:

```java
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

```java
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.

```java
// 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`.

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.