Tooltip System
RegistryLib extends vanilla Minecraft tooltips with a two-level model:
SubNodeis the content you want to show.RootNodeis the container that decides where that content is rendered.
That extra structure gives you four things vanilla tooltips do not have:
- ordering by priority
- automatic separator lines
- extra tooltip boxes below the vanilla box
- custom-rendered content such as bars, icons, or swatches
If you only need one sentence under the item name, use item.tooltip(Component). If you need layout, ordering, or custom visuals, use tooltip nodes.
For general item registration, also see Register Items.
Quick Start
If you are new to the system, start here.
public static final RootNodeRef DETAIL_BOX = TooltipRegistry.rootNode(
"mymod:detail_box", 10, true);
item.tooltip((collector, stack) -> {
collector.node(
new SubNode.Basic(Component.literal("§dMagic Wand"), 0),
true, false);
collector.node(
new SubNode.Basic(
Component.literal("§7Durability: §f"
+ (stack.getMaxDamage() - stack.getDamageValue())),
10));
collector.node(
DETAIL_BOX,
new SubNode.Basic(Component.literal("§bDetailed Information"), 0));
collector.node(
DETAIL_BOX,
new SubNode.Basic(Component.literal("§7Fire resistant"), 10));
});
What this does:
- Adds two inline lines to the vanilla tooltip area.
- Inserts a separator above the first custom inline line.
- Creates a second tooltip box below the vanilla one for extra details.
- Sorts all custom lines by priority within their own container.
Use this mental shortcut:
- same box as vanilla -> default root
- different box below vanilla -> custom
RootNodeRef - text line ->
SubNode.Basic - custom visual element -> extend
SubNode
Examples use Component.literal(...) for brevity. In a real mod, prefer Component.translatable(...) when the text should be localizable.
Choose the Right Approach
| Goal | API | Use it when |
|---|---|---|
| Add one static line | item.tooltip(Component) | You just want a short description. |
| Add dynamic lines | item.tooltip((collector, stack) -> ...) | Content depends on ItemStack, durability, NBT, mode, or attachments. |
| Add a visual break from vanilla lines | collector.node(node, true, false) | You want your custom section to read like a separate block. |
| Show extra information in another box | TooltipRegistry.rootNode(..., true) | Secondary details should not crowd the main tooltip. |
| Add tooltip logic from an attachment | collectTooltipNodes(...) | Tooltip content belongs to a reusable CompositeItemAttachment. |
| Render icons or bars | custom SubNode | Text is not enough. |
Core Mental Model
Vanilla Minecraft treats a tooltip as a flat list of lines inside one dark box. RegistryLib does not replace that system. It injects additional tooltip content during NeoForge’s RenderTooltipEvent.GatherComponents, so your content appears alongside the vanilla tooltip.
Think of the RegistryLib model like this:
SubNode: one renderable unit, such as a text line or progress barRootNode: one tooltip region that holds subnodesRootNodeRef: the handle you keep in registration codeTooltipNodeCollector: the object you write nodes into during tooltip construction
SubNode
SubNode is the smallest renderable part of the tooltip. The built-in SubNode.Basic wraps a Component.
Each subnode has a priority:
new SubNode.Basic(Component.literal("§dTitle"), 0);
new SubNode.Basic(Component.literal("§7Details"), 10);
Lower priority values render higher within the same root.
RootNode
RootNode decides where a group of subnodes is rendered.
| Mode | separateBox | Result |
|---|---|---|
| Inline | false | Content is appended inside the vanilla tooltip frame. |
| Independent box | true | Content is drawn in its own box below the vanilla tooltip. |
RegistryLib always provides one built-in default root node with separateBox=false. If you call collector.node(subNode), that node goes there.
RootNodeRef
You usually do not construct RootNode directly in item registration code. Instead, create and reuse a RootNodeRef:
public static final RootNodeRef DETAIL_BOX = TooltipRegistry.rootNode(
"mymod:detail_box", 10, true);
Declare it once, usually as static final, and reuse it everywhere that should write into that box.
Separators
Separators are inserted automatically by the registry. You only express the intent:
collector.node(subNode, true, false);
This means:
separatorAbove=true: add a separator before this node when appropriateseparatorBelow=true: add a separator after this node if another node follows
For the default inline root, separatorAbove on the first node is the usual way to visually split your custom section from the vanilla lines.
Step by Step
1. Add a Single Static Line
Use this when all you want is a short description.
item.tooltip(Component.literal("§5A powerful magical artifact"));
This is the simplest API and the right default choice for basic item flavor text.
2. Add Dynamic Multi-Line Content
Use the callback form when the tooltip depends on the current ItemStack.
item.tooltip((collector, stack) -> {
collector.node(
new SubNode.Basic(Component.literal("§dMagic Wand"), 0),
true, false);
collector.node(
new SubNode.Basic(
Component.literal("§7Durability: §f"
+ (stack.getMaxDamage() - stack.getDamageValue())),
10));
});
Why this is useful:
stackgives you access to durability, NBT, custom data, and state- priorities keep the output stable even when several systems contribute nodes
- the separator makes the custom content read like a real section rather than an arbitrary extra line
3. Move Details into a Separate Box
When there is too much information for the main tooltip, split it into another box.
public static final RootNodeRef DETAIL_BOX = TooltipRegistry.rootNode(
"mymod:detail_box", 10, true);
item.tooltip((collector, stack) -> {
collector.node(new SubNode.Basic(Component.literal("§dTitle"), 0), true, false);
collector.node(
DETAIL_BOX,
new SubNode.Basic(Component.literal("§bDetailed Information"), 0));
collector.node(
DETAIL_BOX,
new SubNode.Basic(Component.literal("§7Fire resistant"), 10));
});
Good candidates for separate boxes:
- secondary stats
- debug or dev-only details
- contextual usage instructions
- attachment-provided auxiliary info
4. Add Tooltips to Blocks
Blocks expose item tooltips through their BlockItem, so you configure the tooltip via .item(item -> ...).
block.item(item -> item
.tooltip((collector, stack) -> {
collector.node(
new SubNode.Basic(Component.literal("§5Drops coins when mined")),
true, false);
})
);
All tooltip features available to items also work here, because the block item uses the same item builder pipeline.
5. Let Attachments Contribute Tooltip Content
If you use CompositeItemAttachment, the attachment can provide its own tooltip nodes by overriding collectTooltipNodes(...).
public class InspectAttachment extends CompositeItemAttachment<CompositeItem> {
@Override
public void collectTooltipNodes(
CompositeItem item, ItemStack stack, TooltipNodeCollector collector) {
collector.node(
new SubNode.Basic(Component.literal("§eRight-click to inspect"), 100));
}
}
Then attach it normally:
item.attach(new InspectAttachment());
This is a good fit when tooltip content belongs to behavior that is already encapsulated in an attachment. The attachment’s nodes are merged with all other tooltip sources for that item.
6. Render Custom Visual Content with a SubNode
When text is not enough, extend SubNode directly.
public class ProgressBarNode extends SubNode {
private final float progress;
public ProgressBarNode(float progress, int priority) {
super(priority);
this.progress = progress;
}
@Override
public int getHeight(Font font) {
return 7;
}
@Override
public int getWidth(Font font) {
return 80;
}
@Override
public void renderImage(
Font font, int x, int y, int width, int height, GuiGraphics graphics) {
graphics.fill(x, y + 2, x + 80, y + 5, 0xFF333333);
int fillWidth = (int) (80 * progress);
graphics.fill(x, y + 2, x + fillWidth, y + 5, 0xFF55FF55);
}
}
Use it like any other node:
item.tooltip((collector, stack) -> {
float pct = 1.0f - (float) stack.getDamageValue() / stack.getMaxDamage();
collector.node(new ProgressBarNode(pct, 20));
});
Implementation guidelines:
- implement
getWidth()andgetHeight()accurately, because layout depends on them - put text in
renderText(...) - put shapes, icons, lines, and bars in
renderImage(...) - override only what you need; both render methods are no-ops by default
7. Customize the Box Background
Independent boxes can use a custom background renderer.
public static final RootNodeRef CUSTOM_BOX = TooltipRegistry.rootNode(
"mymod:custom",
5,
true,
6,
(graphics, x, y, w, h) -> {
graphics.fill(x, y, x + w, y + h, 0xCC222222);
});
The default renderer matches the vanilla tooltip look closely: dark background, subtle bright top border, and darker bottom edge.
Copy-Paste Recipes
Recipe: Add a Basic Section Below the Vanilla Lines
item.tooltip((collector, stack) -> {
collector.node(
new SubNode.Basic(Component.literal("§6Special Properties"), 0),
true, false);
collector.node(new SubNode.Basic(Component.literal("§7Fire resistant"), 10));
collector.node(new SubNode.Basic(Component.literal("§7Unbreakable in lava"), 20));
});
Use this for the most common “append a mini section” case.
Recipe: Keep Main Tooltip Short, Put Details Elsewhere
public static final RootNodeRef DETAIL_BOX = TooltipRegistry.rootNode(
"mymod:details", 10, true);
item.tooltip((collector, stack) -> {
collector.node(
new SubNode.Basic(Component.literal("§aPortable Generator"), 0),
true, false);
collector.node(DETAIL_BOX,
new SubNode.Basic(Component.literal("§7Output: §f80 FE/t"), 0));
collector.node(DETAIL_BOX,
new SubNode.Basic(Component.literal("§7Buffer: §f100000 FE"), 10));
});
Use this when the item should stay readable at a glance.
Recipe: Tooltip from an Attachment
public class ChargeAttachment extends CompositeItemAttachment<CompositeItem> {
@Override
public void collectTooltipNodes(
CompositeItem item, ItemStack stack, TooltipNodeCollector collector) {
collector.node(new SubNode.Basic(Component.literal("§bCharge module installed"), 50));
}
}
Use this when tooltip content should travel with the attachment rather than the item registration itself.
Full Example
This example combines static text, dynamic lines, a separate detail box, and attachment-contributed content.
public class FullItemExample {
public static final RootNodeRef DETAIL_BOX = TooltipRegistry.rootNode(
"mymod:detail_box", 10, true);
static class InspectAttachment extends CompositeItemAttachment<CompositeItem> {
@Override
public InteractionResult use(
CompositeItem item, Level level, Player player, InteractionHand hand) {
if (!level.isClientSide()) {
player.sendSystemMessage(Component.literal("Inspecting magic wand..."));
}
return InteractionResult.SUCCESS;
}
@Override
public void collectTooltipNodes(
CompositeItem item, ItemStack stack, TooltipNodeCollector collector) {
collector.node(
new SubNode.Basic(Component.literal("§eRight-click to inspect"), 100));
}
}
public static final ItemEntry<CompositeItem> MAGIC_WAND =
RegistryLibTest.REGISTRYLIB
.item("magic_wand", CompositeItem::new)
.initialProperties(() -> new Item.Properties().stacksTo(1))
.properties(p -> p.fireResistant())
.lang("Magic Wand")
.defaultModel()
.tab(CreativeModeTabs.TOOLS_AND_UTILITIES)
.tag(ItemTags.DURABILITY_ENCHANTABLE)
.tooltip(Component.literal("§5A powerful magical artifact"))
.tooltip((collector, stack) -> {
collector.node(
new SubNode.Basic(Component.literal("§dMagic Wand"), 0),
true, false);
collector.node(
new SubNode.Basic(
Component.literal("§7Durability: §f"
+ (stack.getMaxDamage()
- stack.getDamageValue())),
10));
collector.node(
DETAIL_BOX,
new SubNode.Basic(
Component.literal("§bDetailed Information"), 0));
collector.node(
DETAIL_BOX,
new SubNode.Basic(Component.literal("§7Fire resistant"), 10));
})
.attach(new InspectAttachment())
.register();
}
Resulting layout:
- Vanilla tooltip area shows the item name, the static tooltip line, a separator, the inline custom lines, and the attachment line.
- A second bordered box below shows the extra detail lines.
How Ordering and Layout Work
Understanding these rules will prevent most confusion.
Ordering
- nodes are grouped by
RootNodeRef - nodes are sorted by
SubNode.priorityinside each root - lower priority values render first
- separate boxes are sorted by
RootNode.priority
Separators
- separators are inserted by the registry, not by user code
- a separator appears between two nodes if the previous node requested
separatorBelowor the next node requestedseparatorAbove - for the default root,
separatorAboveon the first node is how you create a visual break from vanilla content
Inline vs Separate Box
- inline nodes contribute to the vanilla tooltip’s width and height
- separate boxes render below the vanilla tooltip
- separate boxes use their own padding and background renderer
- multiple separate boxes stack vertically with a small gap
Render Order
Rendering happens in two passes:
renderText(...)draws inline text, then separate box backgrounds, then separate box text.renderImage(...)draws inline graphics, then separate box graphics.
This ordering ensures backgrounds are already in place before custom imagery is drawn on top.
Troubleshooting
My tooltip does not appear
Check these first:
- you registered the tooltip on the item or block item, not only on the block itself
- the item actually reaches the tooltip callback for the hovered stack
- your collector receives at least one node
- your custom nodes report non-zero width and height when they should be visible
My nodes are in the wrong order
Priority is ascending. 0 renders above 10. If content from multiple places is mixing badly, assign a clear priority convention such as 0-49 for title and summary, 50-99 for item internals, and 100+ for attachments.
My separator is missing
Remember that separators are conditional. The most common pattern is:
collector.node(new SubNode.Basic(Component.literal("§6Details"), 0), true, false);
That requests a separator above the first inline node, which creates a break from the vanilla lines.
My separate box is not separate
Make sure the root node was created with separateBox=true:
TooltipRegistry.rootNode("mymod:detail_box", 10, true)
My custom node renders but layout looks wrong
Usually the problem is one of these:
getWidth()is too small, so content gets clipped or overlaps visuallygetHeight()is too small, so the next node renders too early- drawing code assumes a fixed width that does not match the reported width
I have too much tooltip logic in one place
Move reusable behavior into a CompositeItemAttachment and let it contribute via collectTooltipNodes(...).
Best Practices
- Start with
item.tooltip(Component)unless you actually need node-level control. - Use one inline section for the most important information and move secondary details into a separate box.
- Reuse
RootNodeRefvalues instead of creating them ad hoc inside tooltip callbacks. - Prefer clear priority ranges over arbitrary numbers.
- Prefer
Component.translatable(...)for user-facing text in production mods. - Keep custom
SubNodedimensions honest; layout quality depends on them.
Source Reference
If you want to understand or debug the implementation, these are the main entry points in the source tree:
src/main/java/com/gto/registrylib/tooltip/TooltipRegistry.javasrc/main/java/com/gto/registrylib/tooltip/TooltipNodeCollector.javasrc/main/java/com/gto/registrylib/tooltip/SubNode.javasrc/main/java/com/gto/registrylib/tooltip/RootNode.javasrc/main/java/com/gto/registrylib/client/Client.javasrc/main/java/com/gto/registrylib/client/RegistryLibClientTooltip.javasrc/main/java/com/gto/registrylibtest/item/FullItemExample.java
Use them in this order:
FullItemExampleto see intended usage.TooltipRegistryto understand grouping, sorting, and separator insertion.RegistryLibClientTooltipto understand final rendering behavior.
API Reference
TooltipRegistry
Global registry for root nodes and per-item tooltip callbacks.
| Method | Description |
|---|---|
defaultRootRef() | Returns the built-in inline root node reference. |
rootNode(id, priority, separateBox) | Creates a root node with default padding and the default box renderer. |
rootNode(id, priority, separateBox, padding, boxRenderer) | Creates a root node with explicit padding and custom box rendering. |
registerRootNode(ref, rootNode) | Registers a pre-built root node instance manually. |
register(ItemLike, TooltipConfig) | Registers a tooltip callback for an item. Usually called indirectly by ItemBuilder.tooltip(...). |
TooltipNodeCollector
Receives nodes during tooltip assembly.
| Method | Description |
|---|---|
node(SubNode) | Adds a node to the default inline root. |
node(SubNode, separatorAbove, separatorBelow) | Same as above, but requests separators. |
node(RootNodeRef, SubNode) | Adds a node to a specific root. |
node(RootNodeRef, SubNode, separatorAbove, separatorBelow) | Adds a node to a specific root with separator preferences. |
TooltipNodeCollector.TooltipConfig
@FunctionalInterface
public interface TooltipConfig {
void configure(TooltipNodeCollector collector, ItemStack stack);
}
SubNode
Base class for tooltip leaf content.
| Member | Description |
|---|---|
SubNode(int priority) | Constructor. Lower value means earlier rendering inside the same root. |
getHeight(Font) | Returns this node’s height in pixels. |
getWidth(Font) | Returns this node’s width in pixels. |
renderText(GuiGraphics, Font, x, y) | Draws text content. Default no-op. |
renderImage(Font, x, y, width, height, GuiGraphics) | Draws graphics. Default no-op. |
SubNode.Basic
Built-in text node for Component content.
| Constructor | Description |
|---|---|
Basic(Component text) | Creates a text node with default priority 0. |
Basic(Component text, int priority) | Creates a text node with explicit priority. |
RootNode
Defines how a group of subnodes is rendered.
| Property | Meaning |
|---|---|
id | Unique string identifier. |
priority | Order among separate boxes. |
separateBox | Whether the root renders in an independent box. |
padding | Inner padding used for separate-box rendering. |
boxRenderer | Background renderer for separate boxes. |
RootNodeRef
Lightweight handle for a root node. Keep it as a reusable constant and pass it to collector.node(ref, ...).
CompositeItemAttachment.collectTooltipNodes(...)
Override this in attachments when the attachment should contribute tooltip nodes. RegistryLib detects the override automatically and merges the result with the item’s other tooltip sources.