Skip to content

· Tutorial

Custom Minecraft item: name, lore, effects and recipe

Custom Minecraft item with a coloured display name, lore lines and an enchantment glint

A one of a kind item, with its own name, its own backstory in the lore, its own damage number and its own recipe, is the single most requested feature on a server that wants to feel different. The code is about fifty lines. What breaks is almost never the logic, it is the version. And in 2026 the exact spot where it breaks is the first line you will write: the display name.

This guide covers the name and lore, enchantments, attributes, recognising the item later in your own code, and recipes. It also draws the line, row by row, between what the server can do on its own and what needs a resource pack sitting on every player's machine.

Start with where servers actually are

Minecraft moved to calendar versioning. The latest release is 26.2, out on 16 June 2026. But the public sample from bStats, across 62,948 servers, tells a very different story from the download page:

  • 1.21.11: 36.4% of the fleet, by a wide margin the most common.
  • 26.1.2: 14.9%.
  • 26.2: 7.4%, five times smaller than 1.21.11 on its own.
  • 1.21.4: 7.2%.

The 1.21 family is still the majority, and Paper 26.2 has no stable build at all, which makes 26.1.2 the sensible production target on the calendar side. That split is not trivia, it decides your code. A snippet copied from a 1.21 tutorial and a snippet copied from a 26.x tutorial cannot both be correct, and those numbers tell you which of the two worlds your players live in.

The build rule that solves most of this

Compile against the Paper 1.21.11 API, target Java 21, and only call API that exists in both families. Java 25 is the floor for calendar versions, but a Java 21 .jar loads on a newer runtime, and the reverse does not hold. You end up with one file that serves both halves of the fleet instead of one file that serves 7.4% of it.

One more structural change worth knowing: Minecraft has been deobfuscated since 26.1, and the Paper remapper is gone. Good news generally, but specifically for items it means you have no remaining reason to drop down to internals. Everything in this article uses the public API documented on docs.papermc.io and jd.papermc.io.

Display name and lore: precisely where Adventure 5 breaks you

Paper 26.x ships Adventure 5, and Adventure 5 removed API that had previously only been deprecated. The removals cluster around messages and text shown to players. An item display name and its lore are, of course, text shown to players. That is why this apparently trivial topic is the first thing to fall over: code written for 1.21 can stop compiling on line three.

Java
// The old road: plain strings and legacy colour codes.
// This is what most of the tutorials still online will hand you.
meta.setDisplayName(ChatColor.GOLD + "Void Blade");
meta.setLore(Arrays.asList(ChatColor.GRAY + "Forged inside a pocket of air"));

// The road that compiles on the 1.21 API AND on Paper 26.x: Component.
meta.displayName(Component.text("Void Blade", NamedTextColor.GOLD)
        .decoration(TextDecoration.ITALIC, false));
meta.lore(List.of(
        Component.text("Forged inside a pocket of air", NamedTextColor.GRAY)
                .decoration(TextDecoration.ITALIC, false)));

The bit everyone forgets is decoration(TextDecoration.ITALIC, false). The client italicises every custom name and every lore line by default. Without that call, your legendary weapon renders leaning over like something a player scribbled at an anvil, and it is immediately obvious. It is also the one genuinely concrete win of the Component road over legacy strings: a legacy string cannot turn italics off.

Adventure is included in Paper, so there is nothing to bundle and nothing to declare. And because MiniMessage comes with it, a gradient is one line instead of a hand rolled loop over colour codes.

Java
package fun.minax.artifacts;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.EquipmentSlotGroup;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.Plugin;

import java.util.List;

public final class VoidBlade {

    /** Item schema version. Bump it on every rebalance. */
    public static final int SCHEMA = 2;
    public static final String ITEM_ID = "void_blade";

    private static final MiniMessage MM = MiniMessage.miniMessage();

    private final NamespacedKey idKey;
    private final NamespacedKey schemaKey;
    private final NamespacedKey damageKey;

    public VoidBlade(Plugin plugin) {
        this.idKey = new NamespacedKey(plugin, "item_id");
        this.schemaKey = new NamespacedKey(plugin, "item_schema");
        this.damageKey = new NamespacedKey(plugin, "void_blade_damage");
    }

    public ItemStack create() {
        ItemStack item = new ItemStack(Material.NETHERITE_SWORD);
        ItemMeta meta = item.getItemMeta();

        meta.displayName(MM.deserialize("<gradient:#8f5bff:#25d0ff>Void Blade</gradient>")
                .decoration(TextDecoration.ITALIC, false));

        meta.lore(List.of(
                Component.text("Forged inside a pocket of air.", NamedTextColor.GRAY)
                        .decoration(TextDecoration.ITALIC, false),
                Component.empty(),
                Component.text("Damage  +12", NamedTextColor.DARK_AQUA)
                        .decoration(TextDecoration.ITALIC, false)));

        meta.addEnchant(Enchantment.SHARPNESS, 5, true);
        meta.setEnchantmentGlintOverride(Boolean.TRUE);

        meta.addAttributeModifier(
                Attribute.ATTACK_DAMAGE,
                new AttributeModifier(damageKey, 12.0,
                        AttributeModifier.Operation.ADD_NUMBER,
                        EquipmentSlotGroup.MAINHAND));
        meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES);

        meta.getPersistentDataContainer().set(idKey, PersistentDataType.STRING, ITEM_ID);
        meta.getPersistentDataContainer().set(schemaKey, PersistentDataType.INTEGER, SCHEMA);

        item.setItemMeta(meta);
        return item;
    }
}

Do not build against 26.x if you plan to distribute

If you compile your .jar against the 26.x API and touch anything absent from 1.21, your plugin will not load on 36.4% of servers. The other direction is comfortable: code that only uses Component runs on both sides. That is the only reason I tell people to compile against the oldest version they intend to serve, not the newest one they happen to own.

Enchantments and glint

An enchantment beyond the vanilla cap goes on with addEnchant(enchantment, level, true), the third argument being the permission to exceed the game limits. Nothing hard here except the name of the constant.

Enchantments became registry entries, and the API constants followed the in game identifiers. An old snippet that writes Enchantment.DAMAGE_ALL no longer compiles on a recent API where the entry is called Enchantment.SHARPNESS. Always check the constant in the javadoc for your target version on jd.papermc.io or hub.spigotmc.org. And if you want code that survives the next rename, resolve the enchantment by key instead of by static field:

Java
// Rename proof: the vanilla key itself does not move.
Enchantment sharpness = Registry.ENCHANTMENT.get(NamespacedKey.minecraft("sharpness"));
if (sharpness != null) {
    meta.addEnchant(sharpness, 5, true);
}

For the glint on its own, with no real enchantment, setEnchantmentGlintOverride(true) does the job. It is far cleaner than the old trick of a dummy enchantment hidden behind an ItemFlag, and it adds no side effect. Conversely, if you want to keep a real enchantment but hide its tooltip line so you can write the wording yourself, ItemFlag.HIDE_ENCHANTS is still the answer. No resource pack in either case.

Attributes: the item's real numbers

An attribute is a value the game already understands: attack damage, movement speed, armour, reach. You change them with an AttributeModifier bound to a slot group, which avoids the all time classic of a sword handing out its bonuses from inside a backpack.

Two traps. The first mirrors enchantments: attribute constants lost their historical prefix when they joined a registry, so a snippet that writes Attribute.GENERIC_ATTACK_DAMAGE can fail to compile where the API now expects Attribute.ATTACK_DAMAGE. The second is the constructor: the old UUID plus free text form is deprecated in favour of a NamespacedKey plus an EquipmentSlotGroup. Give every modifier its own distinct key, otherwise you will not be able to remove it cleanly when you rebalance.

What an attribute will never do

Attributes only cover what the game already exposes. Steal two hearts on hit, call lightning one time in ten, burn the undead: none of that is an attribute. Those are event listeners in your code, and every one of them starts with the same question. Is the item this player is holding actually mine?

Recognising the item later: the part that actually matters

Nearly every hobby item plugin shares one flaw: it identifies the item by its display name. That is fragile twice over. A player can rename any sword at an anvil, and more importantly, the day you fix a typo in your own wording, every copy already in circulation stops being recognised. You just broke your players' inventories with a copy edit.

The right answer is the ItemMeta PersistentDataContainer: typed keys, namespaced to your plugin, stored on the item itself. They survive chests, shulkers, ground drops and server restarts, and the player never sees them.

Java
package fun.minax.artifacts;

import org.bukkit.NamespacedKey;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.Plugin;

public final class VoidBladeListener implements Listener {

    private final NamespacedKey idKey;
    private final NamespacedKey schemaKey;

    public VoidBladeListener(Plugin plugin) {
        this.idKey = new NamespacedKey(plugin, "item_id");
        this.schemaKey = new NamespacedKey(plugin, "item_schema");
    }

    @EventHandler
    public void onInteract(PlayerInteractEvent event) {
        ItemStack item = event.getItem();
        if (item == null || !item.hasItemMeta()) {
            return;
        }

        PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
        if (!VoidBlade.ITEM_ID.equals(pdc.get(idKey, PersistentDataType.STRING))) {
            return;
        }

        Integer schema = pdc.get(schemaKey, PersistentDataType.INTEGER);
        if (schema == null || schema < VoidBlade.SCHEMA) {
            // An older generation copy: upgrade it in place rather than letting two
            // different balance passes coexist on the same server.
            migrate(item);
            return;
        }

        triggerEffect(event);
    }

    private void migrate(ItemStack legacy) {
        // Re-apply lore, attributes and the current schema onto the existing copy.
    }

    private void triggerEffect(PlayerInteractEvent event) {
        // The effect specific to this item.
    }
}

The item_schema key is the detail almost nobody writes, and the one that will save you. Six months after release you will want to lower the damage. The copies already crafted are asleep in chests with the old modifier baked into them. With no version number stored on the item you have no way to tell them from fresh ones, and your rebalance only applies to new players. With one, you upgrade them on the first click.

The database trap: 75.1% of servers run in offline mode

The next instinct is a table like item_owner(player_uuid, item_id). That is the number one trap when designing storage for a plugin. 75.1% of servers run in offline mode, and in that mode a player's UUID is derived from their username. The day the player changes name, or the day the server flips to online mode, the key changes and every row is orphaned. The item has no username: keep its identity on the item, in the container, and index on that identity if you genuinely need a database.

My call here is blunt. As long as your question is "is this item mine, and which version is it", the container is enough and you need no database at all. A database becomes the better tool at the exact moment the question inverts into "who is holding the three unique copies I handed out". That needs an index. The SQLite JDBC driver is already shipped with Paper, so it costs you no bundled dependency. HikariCP is not shipped: if you want a connection pool, declare it in the libraries: block of your plugin.yml and let the server download it, instead of burying it in your .jar. Either way, never touch the database on the main thread, or you will watch the TPS fall.

If you also target Folia, remember that world regions run across several threads: a plugin that assumes a single main thread causes crashes. Anything you schedule around the item goes through the RegionScheduler, not a global scheduler.

Custom recipes

A shaped recipe is registered at startup under a key owned by your plugin. The result stack you hand it keeps its ItemMeta, so the item a player crafts carries your persistent data, your lore and your attributes.

Java
NamespacedKey recipeKey = new NamespacedKey(this, "void_blade");

ShapedRecipe recipe = new ShapedRecipe(recipeKey, new VoidBlade(this).create());
recipe.shape(" I ", " I ", " R ");
recipe.setIngredient('I', Material.NETHERITE_INGOT);
recipe.setIngredient('R', Material.BREEZE_ROD);

// Idempotent: without the removal, registering the same key twice fails.
Bukkit.removeRecipe(recipeKey);
Bukkit.addRecipe(recipe);

Three things the docs underplay. First, the removal before the add: without it, a second registration of the same key is a problem, and that is exactly what happens when somebody runs /reload instead of restarting. Second, discovery: the recipe works for anyone who knows the shape, but it only appears in the recipe book after a discoverRecipe call on the player. Third, and most annoying, the real limit.

A ShapedRecipe reasons about material, not about your persistent data. So it alone cannot demand that an ingredient be your own custom item. The reliable pattern is to register the recipe on the base material and then validate the marker at craft time:

Java
@EventHandler
public void onPrepare(PrepareItemCraftEvent event) {
    Recipe recipe = event.getRecipe();
    if (!(recipe instanceof Keyed keyed) || !recipeKey.equals(keyed.getKey())) {
        return;
    }

    boolean fragmentPresent = false;
    for (ItemStack slot : event.getInventory().getMatrix()) {
        if (slot == null || !slot.hasItemMeta()) {
            continue;
        }
        String id = slot.getItemMeta().getPersistentDataContainer()
                .get(idKey, PersistentDataType.STRING);
        if ("void_fragment".equals(id)) {
            fragmentPresent = true;
        }
    }

    if (!fragmentPresent) {
        event.getInventory().setResult(null);
    }
}

Test the anvil, the grindstone and the smithing table

Any vanilla station that rebuilds an item can hand back a copy without your marker: anvil repairs, grindstone disenchanting, smithing table upgrades. Do not assume, push your item through each station before you open the server, and if the marker disappears, simply block that station for this item.

What needs a resource pack, and what does not

This is the most frequently asked question, and the answer is friendlier than most people assume. Almost everything is server side.

Server aloneResource pack required
Name, lore, colours, gradientsYesNo
Glint, hidden enchantmentsYesNo
Attributes, durability, stack sizeYesNo
Persistent data, recipeYesNo
Particles, vanilla soundsYesNo
Its own texture and 3D modelImpossibleRequired
An original soundImpossibleRequired

For a texture the mechanism has two halves and you only control one. Server side you set a custom model data value on the item. Client side a resource pack has to map that value to a model. The hard part: you can set the value and it will do absolutely nothing without the pack. A player who declines the download sees a plain netherite sword wearing your name and your lore. The item component format is documented on minecraft.wiki, and pack delivery is configured in server.properties or per player from your plugin.

The advice I give small servers

If you cannot enforce the pack, do not hang your item's identity on its texture. Hang it on the name, the lore, the glint, click particles and one well chosen vanilla sound. An item that reads as special without a pack reads as special for every one of your players, including the one behind a network that blocks downloads.

And if drawing 16x16 pixel art is not your thing, Minax can generate the texture with AI and build the resource pack around it, wired to your item's custom model data.

What will break on you

  • Italic lore. You forgot the call that turns the decoration off. Three seconds to spot, one line to fix.
  • The snippet from an old tutorial. Prefixed attribute constants, pre registry enchantment names, text calls removed by Adventure 5. Three families of compile error, one cure: the javadoc for your target version.
  • Identification by name. Works for a week, then a player renames a sword at an anvil or you fix a typo.
  • A table keyed on the player UUID. In offline mode, which is 75.1% of servers, that UUID is derived from the username. One rename and your rows are orphans.
  • The recipe registered twice. One /reload and the key already exists. Remove before adding, and restart instead of reloading.
  • The texture that only works for you. Your pack is installed locally and you forget it is not being served to anyone else.
  • Database work on the main thread, or a global scheduler on Folia. The first drops your TPS, the second crashes the server.

My verdict

Under 50 concurrent players: one plugin, everything in the PersistentDataContainer, no database, no mandatory resource pack. Built against the Paper 1.21.11 API targeting Java 21, with Adventure Component for all text. That .jar runs on the 36.4% of the fleet on 1.21.11 as happily as on the 14.9% on 26.1.2, and you will not have to rebuild it for a long time.

Above that, or as soon as unique items carry value: add a SQLite index, whose driver already sits inside Paper, with a key that comes from the item and never from the player, and with every access off the main thread. At that size you can also make the resource pack mandatory: players of an established server accept the download, players of an unknown one do not.

Only target 26.2 exclusively if: the server is yours, you accept rebuilding on every build, and you have no intention of distributing the .jar. Paper 26.2 has no stable build, and 26.2 is 7.4% of the fleet. That is not a distribution target, it is a playground.

Your custom item, compiled and tested

Describe the item: its name, its lore, its effects, its recipe. Minax writes the Java, compiles it for your target version and hands you a .jar ready to drop into /plugins/.

Create my item

Frequently asked questions

Do I need a resource pack to make a custom item?

Not unless you want a different texture or 3D model. The coloured name, the lore, enchantments, glint, attributes, persistent data and the crafting recipe are all server side. Only a visual that differs from the base material needs a resource pack installed on every client.

Why does setDisplayName no longer compile on Paper 26?

Paper 26.x ships Adventure 5, which removed API that had previously only been deprecated, mostly around messages and text shown to players. Item display name and lore now go through Component, with displayName(Component) and lore(List<Component>), both of which also exist on the 1.21 API.

How do I recognise my custom item later in code?

Use the ItemMeta PersistentDataContainer under a NamespacedKey owned by your plugin. Never match on the display name: a player can rename any item at an anvil, and the day you fix a typo in your own wording, every copy already in circulation stops being recognised.

Which Java version should a 2026 plugin target?

Java 25 is the floor for the calendar versions of Minecraft, but compiling for Java 21 is still the right call: it is what most of the server fleet runs, and a Java 21 .jar loads fine on a newer runtime. The reverse is not true.

Can a recipe require a custom item as an ingredient?

Not with a ShapedRecipe alone, because it matches on material, not on your persistent data. The reliable pattern is to register the recipe on the base material, then verify your marker in PrepareItemCraftEvent and clear the result when it is missing.

Keep going: if the plugin refuses to start, work through the troubleshooting guide, and if you are still weighing a script against a real .jar, read Skript vs a compiled plugin.

Related articles