Text and colors with Adventure
Components: colored, styled, clickable text the modern way.
Every message a plugin shows, from a chat line to an item name, is a piece of formatted text. On this page you will build colored, bold, clickable text with Adventure components, add hover tooltips, send it to players and the console, and convert old &a color codes into the modern format.
Why text is not just a String
If you have played on older servers, you have seen color codes such as &6 for gold or &l for bold. Those codes are glued into a plain String (a piece of text in Java, see Working with text). Behind the scenes the game writes them with the section sign: §6Welcome, §b§lSteve. This old way has real problems:
- A code switches a style on, and you must remember to switch it off again, or the color leaks into the next word.
- A
Stringcannot hold a click action or a hover tooltip. Plugins used a second, clumsy library for that. - There are only 16 colors, and hex colors needed a long, ugly code such as
§x§f§f§8§8§0§0. - Players can type the codes themselves, so a chat message may suddenly turn rainbow-colored when you did not want that.
The game itself does not really send color codes to players. It sends text as a small tree of pieces, and each piece knows its own color, its own style and what happens when you click or hover it. Paper includes a library called Adventure that builds exactly this kind of text. The objects it builds are called components.
What a component is
A component is one piece of text plus a style. The style holds the color, bold, italic and so on. A component can also have child components, which are more pieces that come after it. A child that has no color of its own borrows the color of its parent. This is called inheritance, the same word Java uses for classes, but here it simply means "children copy the parent's style unless they say otherwise".
This tree idea explains almost every surprise you will meet later. If you color the parent red and append a child, the child is red too. If you want a child to look different, give it its own style.
Your first component message
The next listener sends two messages to every player who joins. Read it once, then look at the notes.
Where this file livestext-adventure-firstsrcmainjavacomexampletextadventurefirstWelcomeListener.java
The package com.example.textadventurefirst is the folder path com/example/textadventurefirst inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- text-adventure-first/
- src/main/
- java/com/example/textadventurefirst/Package com.example.textadventurefirst
- TextFirstPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- WelcomeListener.javayou are hereListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/textadventurefirst/Package com.example.textadventurefirst
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.textadventurefirst;2 3import net.kyori.adventure.text.Component;4import net.kyori.adventure.text.format.NamedTextColor;5import net.kyori.adventure.text.format.TextColor;6import net.kyori.adventure.text.format.TextDecoration;7import org.bukkit.entity.Player;8import org.bukkit.event.EventHandler;9import org.bukkit.event.Listener;10import org.bukkit.event.player.PlayerJoinEvent;11 12public final class WelcomeListener implements Listener {13 14 private static final TextColor ORANGE = TextColor.color(255, 136, 0);15 16 @EventHandler17 public void onJoin(PlayerJoinEvent event) {18 Player player = event.getPlayer();19 20 Component greeting = Component.text("Welcome, ", NamedTextColor.GOLD)21 .append(Component.text(player.getName(), NamedTextColor.AQUA, TextDecoration.BOLD))22 .append(Component.text("!"));23 player.sendMessage(greeting);24 25 Component tip = Component.text("Tip: ", ORANGE)26 .append(Component.text("type /rules to read the rules.", NamedTextColor.GRAY));27 player.sendMessage(tip);28 }29}- A custom color made from red, green and blue numbers from 0 to 255. Orange is not one of the 16 named colors, so we build it.
static finalmeans one shared, never-changing value. - Builds the message piece by piece.
Component.text("Welcome, ", NamedTextColor.GOLD)is the parent: the words, plus the color gold. - A child. It sets its own color and bold, so it ignores the parent's gold.
TextDecoration.BOLDis one of the style switches. - A child with no style. It copies the parent, so the exclamation mark is gold.
- Sends the finished component to this one player.
sendMessageaccepts aComponentdirectly. - A second message. Its first piece uses the custom orange, and its second piece is plain gray.
The main class only has to register the listener, exactly as you learned in Events and listeners:
Where this file livestext-adventure-firstsrcmainjavacomexampletextadventurefirstTextFirstPlugin.java
The package com.example.textadventurefirst is the folder path com/example/textadventurefirst inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- text-adventure-first/
- src/main/
- java/com/example/textadventurefirst/Package com.example.textadventurefirst
- TextFirstPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- WelcomeListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/textadventurefirst/Package com.example.textadventurefirst
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.textadventurefirst;2 3import org.bukkit.plugin.java.JavaPlugin;4 5public final class TextFirstPlugin extends JavaPlugin {6 7 @Override8 public void onEnable() {9 getServer().getPluginManager().registerEvents(new WelcomeListener(), this);10 }11}- Tells Paper that
WelcomeListenerwants to hear about events. Without this line, the greeting never appears.
When Steve joins, he sees:
Welcome, Steve!
Tip: type /rules to read the rules.
Components never change: they are immutable
This is the most important rule on this page. A component is immutable, which means it can never be changed after it is created. Every method that looks like it changes a component, such as color(...) or append(...), actually returns a new component and leaves the old one alone. If you throw the result away, nothing happens.
1Component message = Component.text("Hello");2message.color(NamedTextColor.RED);3player.sendMessage(message);- This builds a red copy and then throws it away. The player sees white text.
The fix is to keep the result, either by assigning it back or by chaining the calls:
1Component message = Component.text("Hello").color(NamedTextColor.RED);2 3Component other = Component.text("Hello");4other = other.color(NamedTextColor.RED);Immutability sounds like a limitation, but it makes components safe: you can keep one in a constant such as ORANGE above, send the same component to a hundred players, and nobody can change it by accident.
Colors
There are three ways to pick a color.
The 16 named colors
NamedTextColor has one constant per classic Minecraft color. These are the exact colors the game uses for chat:
| Constant | Hex | Constant | Hex |
|---|---|---|---|
BLACK | #000000 | DARK_GRAY | #555555 |
DARK_BLUE | #0000AA | BLUE | #5555FF |
DARK_GREEN | #00AA00 | GREEN | #55FF55 |
DARK_AQUA | #00AAAA | AQUA | #55FFFF |
DARK_RED | #AA0000 | RED | #FF5555 |
DARK_PURPLE | #AA00AA | LIGHT_PURPLE | #FF55FF |
GOLD | #FFAA00 | YELLOW | #FFFF55 |
GRAY | #AAAAAA | WHITE | #FFFFFF |
DARK_RED DARK_PURPLE GOLD GRAY
DARK_GRAY BLUE GREEN AQUA
RED LIGHT_PURPLE YELLOW WHITE
Any color you like
Modern Minecraft shows any of 16 million colors. You make one with TextColor, either from three numbers (red, green and blue, each 0 to 255) or from a hex code, the same six-digit color notation web pages use:
1TextColor orange = TextColor.color(255, 136, 0);2TextColor sameOrange = TextColor.color(0xFF8800);3TextColor fromText = TextColor.fromHexString("#ff8800");All three lines make the same color. fromHexString is handy when the color comes from a config file as text. It returns null when the text is not a valid hex color, so check for that when the text comes from a player or a file. NamedTextColor is a kind of TextColor, so every method that wants a TextColor also accepts a named color.
Where do colors go?
Pass a color as the second argument of Component.text(...), or call .color(...) on an existing component. Both build the same thing.
Decorations: bold, italic and friends
Text styles are called decorations. TextDecoration has five: BOLD, ITALIC, UNDERLINED, STRIKETHROUGH and OBFUSCATED (the scrambled "magic" text). Add them after the color in Component.text(...), or switch them on and off later:
1Component title = Component.text("Server rules", NamedTextColor.GOLD, TextDecoration.BOLD, TextDecoration.UNDERLINED);2Component plain = title.decoration(TextDecoration.BOLD, false);3Component italic = Component.text("Psst").decorate(TextDecoration.ITALIC);Building longer messages
Chaining .append(...) works, but long chains get hard to read. A builder is a helper object you add pieces to, and then call build() on to get the finished component. Component.text() with no arguments starts a builder. Here is the first half of the /rules command:
Where this file livestext-adventure-demosrcmainjavacomexampletextadventuredemoRulesCommand.java
The package com.example.textadventuredemo is the folder path com/example/textadventuredemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- text-adventure-demo/
- src/main/
- java/com/example/textadventuredemo/Package com.example.textadventuredemo
- RulesCommand.javayou are hereCommand (BasicCommand)
- ShowItemCommand.javaCommand (BasicCommand)
- TextAdventureDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- WhoCommand.javaCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/textadventuredemo/Package com.example.textadventuredemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
21@Override22public void execute(CommandSourceStack source, String[] args) {23 TextComponent.Builder message = Component.text();24 message.append(Component.text("Server rules", NamedTextColor.GOLD, TextDecoration.BOLD));25 26 for (int i = 0; i < RULES.size(); i++) {27 message.append(Component.newline());28 message.append(Component.text((i + 1) + ". ", NamedTextColor.YELLOW));29 message.append(Component.text(RULES.get(i), NamedTextColor.WHITE));30 }31 32 message.append(Component.newline());33 message.append(buttons());34 source.getSender().sendMessage(message.build());35}- The builder: an empty message that you add pieces to. A builder is not immutable; it is a work in progress.
- The title line. A component can be added to a builder with
append. - A loop adds one line per rule. The numbers 1, 2, 3 come from
i + 1, because list positions start at 0. - A component that is just a line break. Messages do not wrap on their own; you add the breaks.
- The buttons are built in a separate method, so this method stays short. You will read it in the next section.
- Finishes the builder and sends the result. The sender is whoever typed the command: a player or the console.
Useful helpers for combining components:
| Call | What it makes |
|---|---|
Component.empty() | A component with no text. A good starting point when you add children in a loop. |
Component.space() | A single space. |
Component.newline() | A line break. |
Component.join(JoinConfiguration.spaces(), a, b, c) | Puts components in a row with a space between each. JoinConfiguration.separator(x) uses any separator you give it, and JoinConfiguration.commas(true) writes "a, b and c". |
Component.textOfChildren(a, b, c) | Glues components together with no separator, and no style of its own. |
The /rules command, as a player sees it:
1. Be kind to other players.
2. No griefing and no stealing.
3. No cheats or hacked clients.
[I agree] [Website] [Ask staff] [Copy IP]
Click and hover events
The bottom line of the rules message has four buttons. They are ordinary text with two extra attachments:
- A hover event shows a tooltip when the player points the mouse at the text.
- A click event does something when the player clicks the text.
Where this file livestext-adventure-demosrcmainjavacomexampletextadventuredemoRulesCommand.java
The package com.example.textadventuredemo is the folder path com/example/textadventuredemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- text-adventure-demo/
- src/main/
- java/com/example/textadventuredemo/Package com.example.textadventuredemo
- RulesCommand.javayou are hereCommand (BasicCommand)
- ShowItemCommand.javaCommand (BasicCommand)
- TextAdventureDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- WhoCommand.javaCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/textadventuredemo/Package com.example.textadventuredemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
37private static Component buttons() {38 Component agree = Component.text("[I agree]", NamedTextColor.GREEN, TextDecoration.BOLD)39 .hoverEvent(HoverEvent.showText(Component.text("Click to accept the rules", NamedTextColor.GRAY)))40 .clickEvent(ClickEvent.callback(audience ->41 audience.sendMessage(Component.text("Thanks! Have fun on the server.", NamedTextColor.GREEN))));42 43 Component website = Component.text("[Website]", NamedTextColor.AQUA)44 .hoverEvent(HoverEvent.showText(Component.text("Opens papermc.io in your browser", NamedTextColor.GRAY)))45 .clickEvent(ClickEvent.openUrl("https://papermc.io"));46 47 Component askStaff = Component.text("[Ask staff]", NamedTextColor.YELLOW)48 .hoverEvent(HoverEvent.showText(Component.text("Types /msg into your chat box", NamedTextColor.GRAY)))49 .clickEvent(ClickEvent.suggestCommand("/msg "));50 51 Component copyIp = Component.text("[Copy IP]", NamedTextColor.LIGHT_PURPLE)52 .hoverEvent(HoverEvent.showText(Component.text("Copies play.example.com", NamedTextColor.GRAY)))53 .clickEvent(ClickEvent.copyToClipboard("play.example.com"));54 55 return Component.join(JoinConfiguration.spaces(), agree, website, askStaff, copyIp);56}- One button. The text comes first, then the tooltip, then the click action. The order of
hoverEventandclickEventdoes not matter. - The tooltip is a component too, so it can have colors of its own. Always tell players what a click will do.
- Runs your own code when the player clicks. The lambda gets the
Audiencethat clicked, and here it just thanks them. By default a callback can be used only once in total, by whichever player clicks first, and it expires after 12 hours. PassClickCallback.Optionsto allow more uses. - Asks the player's game to open a web address. The player sees a confirmation screen first. Only
https://andhttp://addresses work. - Types text into the player's chat box without sending it. Perfect for "fill in the rest yourself".
- Copies text to the player's clipboard. Great for coordinates, IP addresses and codes.
- Puts the four buttons on one line, separated by spaces.
The click actions you can use:
| Factory method | What happens on click |
|---|---|
ClickEvent.runCommand("/spawn") | The player runs the command as if they had typed it. It only works if they are allowed to use it. Write the leading slash. |
ClickEvent.suggestCommand("/msg Steve ") | The text appears in the player's chat box, ready to finish and send. |
ClickEvent.openUrl("https://papermc.io") | Opens a web page after the player confirms. |
ClickEvent.copyToClipboard("play.example.com") | Copies the text to the clipboard. |
ClickEvent.changePage(2) | Turns to another page in a written book. |
ClickEvent.callback(audience -> ...) | Runs a piece of your plugin's code on the server. |
Hover tooltips can show more than text. Everything below also works on a player's name or an item:
HoverEvent.showText(component)shows a text tooltip.- An
ItemStackcan be passed straight tohoverEvent(item), and the player sees the item's real tooltip with its enchantments. The/showitemcommand below does this. HoverEvent.showEntity(...)shows an entity's name, type and id.
A player list with hover details
The /who command lists everyone online. Each name has a tooltip with their health, level and game mode, and clicking a name starts a private message to them.
Where this file livestext-adventure-demosrcmainjavacomexampletextadventuredemoWhoCommand.java
The package com.example.textadventuredemo is the folder path com/example/textadventuredemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- text-adventure-demo/
- src/main/
- java/com/example/textadventuredemo/Package com.example.textadventuredemo
- RulesCommand.javaCommand (BasicCommand)
- ShowItemCommand.javaCommand (BasicCommand)
- TextAdventureDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- WhoCommand.javayou are hereCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/textadventuredemo/Package com.example.textadventuredemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
33private static Component nameWithDetails(Player player) {34 long health = Math.round(player.getHealth());35 Component details = Component.text()36 .append(Component.text(player.getName(), NamedTextColor.AQUA, TextDecoration.BOLD))37 .append(Component.newline())38 .append(Component.text("Health: ", NamedTextColor.GRAY))39 .append(Component.text(health + " HP", NamedTextColor.RED))40 .append(Component.newline())41 .append(Component.text("Level: ", NamedTextColor.GRAY))42 .append(Component.text(player.getLevel(), NamedTextColor.GREEN))43 .append(Component.newline())44 .append(Component.text("Game mode: ", NamedTextColor.GRAY))45 .append(Component.translatable(player.getGameMode(), NamedTextColor.WHITE))46 .append(Component.newline())47 .append(Component.text("Click to send a message", NamedTextColor.DARK_GRAY, TextDecoration.ITALIC))48 .build();49 50 return Component.text(player.getName(), NamedTextColor.WHITE)51 .hoverEvent(HoverEvent.showText(details))52 .clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));53}- Health is a decimal number such as 18.5.
Math.roundturns it into a whole number, so the tooltip reads "19 HP". - The tooltip is a component with five lines. A
newlinebetween lines makes the box taller. - Components accept numbers directly. You do not need to turn the number into a String first.
- A translatable component: it does not hold the words "Survival" or "Creative", only the game's own key for them. Each player's game shows the word in their own language.
- Clicking a name puts
/msg Stevein the chat box, with the right name filled in.
The main method just collects those pieces and joins them with a gray comma:
Where this file livestext-adventure-demosrcmainjavacomexampletextadventuredemoWhoCommand.java
The package com.example.textadventuredemo is the folder path com/example/textadventuredemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- text-adventure-demo/
- src/main/
- java/com/example/textadventuredemo/Package com.example.textadventuredemo
- RulesCommand.javaCommand (BasicCommand)
- ShowItemCommand.javaCommand (BasicCommand)
- TextAdventureDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- WhoCommand.javayou are hereCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/textadventuredemo/Package com.example.textadventuredemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
19@Override20public void execute(CommandSourceStack source, String[] args) {21 Collection<? extends Player> online = Bukkit.getOnlinePlayers();22 List<Component> names = new ArrayList<>();23 for (Player player : online) {24 names.add(nameWithDetails(player));25 }26 27 Component separator = Component.text(", ", NamedTextColor.GRAY);28 Component message = Component.text("Online (" + online.size() + "): ", NamedTextColor.GOLD)29 .append(Component.join(JoinConfiguration.separator(separator), names));30 source.getSender().sendMessage(message);31}- Everybody currently on the server. The odd
? extendsjust means "some kind of Player"; you can loop over it like a list. - A list that collects one component per player.
- Joins the whole list with the same gray comma between names.
Hover over Steve and this appears (shown here as an item-style tooltip, which looks almost the same):
Translatable names for blocks and items
The game already knows the name of every block, item and game mode in every language. A translatable component lets you reuse them, so a French player reads "Épée en diamant" while an English player reads "Diamond Sword", all from one line of your code:
1Component sword = Component.translatable(Material.DIAMOND_SWORD);2Component mode = Component.translatable(GameMode.CREATIVE);3player.sendMessage(Component.text("You got a ", NamedTextColor.GRAY).append(sword));Under the hood the key is something like item.minecraft.diamond_sword. Material, GameMode and many other Paper types can be passed straight in. This is better than material.name(), which gives the code name DIAMOND_SWORD that no player wants to read. For a whole item, Paper offers item.effectiveName(): the name the player sees in their inventory, which includes a custom name, a potion's name or the vanilla translatable name.
Sending components
In Adventure, anything that can receive messages is an audience. Players are audiences. The console is an audience. Even the whole server is an audience. They all share the same methods, so the same message can go anywhere. The ones you will meet most:
Player: one person.CommandSender: whoever typed a command. That can be a player, the console or a command block. EveryPlayeris also aCommandSender.- The server (
Bukkit.getServer()): every player online at once.
| To send to... | Write |
|---|---|
| One player | player.sendMessage(component) |
| Whoever ran a command | sender.sendMessage(component) |
| The console | Bukkit.getConsoleSender().sendMessage(component) |
| Everyone online, and the console | Bukkit.broadcast(component). Other plugins are told about it and can cancel it. |
| Everyone online, and the console, as one audience | Bukkit.getServer().sendMessage(component). The server object is an audience. |
| Only players with a permission | Bukkit.broadcast(component, "myplugin.admin") |
The /showitem command uses a broadcast to show off the item in your hand. It combines everything so far:
Where this file livestext-adventure-demosrcmainjavacomexampletextadventuredemoShowItemCommand.java
The package com.example.textadventuredemo is the folder path com/example/textadventuredemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- text-adventure-demo/
- src/main/
- java/com/example/textadventuredemo/Package com.example.textadventuredemo
- RulesCommand.javaCommand (BasicCommand)
- ShowItemCommand.javayou are hereCommand (BasicCommand)
- TextAdventureDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- WhoCommand.javaCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/textadventuredemo/Package com.example.textadventuredemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
21@Override22public void execute(CommandSourceStack source, String[] args) {23 if (!(source.getExecutor() instanceof Player player)) {24 source.getSender().sendMessage(Component.text("Only players have hands.", NamedTextColor.RED));25 return;26 }27 ItemStack item = player.getInventory().getItemInMainHand();28 if (item.isEmpty()) {29 player.sendMessage(Component.text("Hold an item in your hand first.", NamedTextColor.RED));30 return;31 }32 33 Component itemName = item.effectiveName().hoverEvent(item);34 Component message = Component.text()35 .append(player.displayName())36 .append(Component.text(" shows off ", NamedTextColor.GRAY))37 .append(Component.text(item.getAmount() + "x ", NamedTextColor.WHITE))38 .append(itemName)39 .build();40 Bukkit.broadcast(message);41 42 String plain = PlainTextComponentSerializer.plainText().serialize(message);43 plugin.getLogger().info("Shown in chat: " + plain);44}- Only a player has a hand. If someone types the command in the console, the code explains that and stops. This
instanceofcheck also gives the variable the nameplayerin one step. - An empty hand counts as air, which has no name or tooltip. Check first, and say what to do.
- The item's name as a component, with the item itself attached as the hover. Hover over it in chat and you see the real tooltip with enchantments.
- The player's chat name, as a component, so it keeps any colors a permissions plugin gave it.
- Sends the message to every player and to the console.
- Turns a component back into plain words with all colors removed. The next section explains why that is useful.
- Writes a line to the server console and log file. Log files are plain text, so we log the plain version.
The plugin class registers all three commands in one place. The commands themselves use BasicCommand, which Commands, part 1 explains in detail:
Where this file livestext-adventure-demosrcmainjavacomexampletextadventuredemoTextAdventureDemoPlugin.java
The package com.example.textadventuredemo is the folder path com/example/textadventuredemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- text-adventure-demo/
- src/main/
- java/com/example/textadventuredemo/Package com.example.textadventuredemo
- RulesCommand.javaCommand (BasicCommand)
- ShowItemCommand.javaCommand (BasicCommand)
- TextAdventureDemoPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- WhoCommand.javaCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/textadventuredemo/Package com.example.textadventuredemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.textadventuredemo;2 3import io.papermc.paper.command.brigadier.Commands;4import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;5import org.bukkit.plugin.java.JavaPlugin;6 7public final class TextAdventureDemoPlugin extends JavaPlugin {8 9 @Override10 public void onEnable() {11 getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {12 Commands commands = event.registrar();13 commands.register("rules", "Show the server rules", new RulesCommand());14 commands.register("who", "List online players, with details when you hover a name", new WhoCommand());15 commands.register("showitem", "Show the item in your hand to everyone", new ShowItemCommand(this));16 });17 }18}- Paper asks plugins to register commands at a special moment during startup. This is where you do it.
- The command's name is the first argument, a description is the second, and the object that runs it is the third.
- This command needs the plugin object to write to the log, so we pass
thisin.
Converting between text formats
Sometimes you have text in another shape: a config file full of &6 codes from an old plugin, or a message you only want to log without colors. A serializer converts between a component and a format. "Serialize" means component to text, "deserialize" means text to component. Paper includes three:
| Serializer | Use it for |
|---|---|
PlainTextComponentSerializer.plainText() | Component to plain words with no colors. For logs, searches and comparing text. |
LegacyComponentSerializer.legacyAmpersand() | Old &a color codes to a component and back. For converting old configs. |
LegacyComponentSerializer.legacySection() | Same, with the real section sign §. |
GsonComponentSerializer.gson() | Component to the game's JSON text format and back. For saving or sending components through code that speaks JSON. |
MiniMessage.miniMessage() | The friendliest format: <green>Hi. It has its own page, MiniMessage. |
Where this file livestext-adventure-convertsrcmainjavacomexampletextadventureconvertTextConvertPlugin.java
The package com.example.textadventureconvert is the folder path com/example/textadventureconvert inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- text-adventure-convert/
- src/main/
- java/com/example/textadventureconvert/Package com.example.textadventureconvert
- TextConvertPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/textadventureconvert/Package com.example.textadventureconvert
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.textadventureconvert;2 3import net.kyori.adventure.text.Component;4import net.kyori.adventure.text.format.NamedTextColor;5import net.kyori.adventure.text.format.TextDecoration;6import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;7import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;8import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;9import org.bukkit.plugin.java.JavaPlugin;10 11public final class TextConvertPlugin extends JavaPlugin {12 13 private static final LegacyComponentSerializer AMPERSAND_CODES = LegacyComponentSerializer.legacyAmpersand();14 15 @Override16 public void onEnable() {17 Component greeting = Component.text("Welcome, ", NamedTextColor.GOLD)18 .append(Component.text("Steve", NamedTextColor.AQUA, TextDecoration.BOLD))19 .append(Component.text("!"));20 21 String plain = PlainTextComponentSerializer.plainText().serialize(greeting);22 String withCodes = AMPERSAND_CODES.serialize(greeting);23 String json = GsonComponentSerializer.gson().serialize(greeting);24 getLogger().info("Plain text: " + plain);25 getLogger().info("Old codes: " + withCodes);26 getLogger().info("JSON: " + json);27 28 Component fromOldConfig = AMPERSAND_CODES.deserialize("&6Hello &b&lworld&r!");29 getServer().broadcast(fromOldConfig);30 }31}- One shared serializer for the
&codes. Serializers are safe to keep and reuse. - Drops all styles and keeps only the words.
- Writes the component using old color codes. Useful when you must feed an old plugin.
- The JSON that Minecraft itself understands.
- Reads an old-style string and builds a component. This is the conversion you do once when you move an old config to the new system.
- Sends the converted message to everyone online, so you can see that the old
&codes became real colors.
Starting this plugin prints these lines in the console:
[14:02:11 INFO]: [TextConvert] Plain text: Welcome, Steve![14:02:11 INFO]: [TextConvert] Old codes: &6Welcome, &b&lSteve&6![14:02:11 INFO]: [TextConvert] JSON: {"color":"gold","extra":[{"bold":true,"color":"aqua","text":"Steve"},"!"],"text":"Welcome, "}If you find old code online that sends text with ChatColor, it looks like this. It still compiles for now, but it is deprecated, and you should write the component version next to it:
1player.sendMessage(ChatColor.GOLD + "Welcome, " + ChatColor.AQUA + ChatColor.BOLD + "Steve");2player.sendMessage(Component.text("Welcome, ", NamedTextColor.GOLD)3 .append(Component.text("Steve", NamedTextColor.AQUA, TextDecoration.BOLD)));Components or MiniMessage?
Everything above builds components in Java. That is precise and safe, and the compiler catches typos. It is also wordy. MiniMessage writes the same text as <gold>Welcome, <aqua><bold>Steve</bold></aqua>! and is the better choice for text players or server owners might edit. Many plugins use both: components for dynamic pieces such as hover tooltips and lists, MiniMessage for the sentences. They produce the same Component type, so you can mix them freely.
Mistakes to avoid
- Forgetting that components are immutable.
message.color(...)alone does nothing. Keep the returned component. - Mixing
StringandComponentin+."Hi " + componentturns the component into a long technical string. Use.append(...). - Using
material.name()for display. Players seeDIAMOND_SWORD. UseComponent.translatable(material). - Clickable text with no hint. Always add a hover that says what the click does.
- Forgetting the slash in
runCommand.runCommand("spawn")sends the word "spawn" as a chat message. WriterunCommand("/spawn"). - Using
callbackfor something that must survive a restart. Callbacks are kept in memory and are lost when the server stops. - Forgetting line breaks. There is no automatic wrapping with
\nin a component tree. UseComponent.newline().
A clickable /where command
Write a command /where that tells the player their block coordinates in aqua and underlined. When the player hovers over the coordinates they see "Click to copy", and clicking copies the coordinates to the clipboard.
Hint 1
You need player.getLocation(), and its methods getBlockX(), getBlockY() and getBlockZ(). Join them into one text such as "12 64 -30".
Hint 2
Build the coordinates component with hoverEvent(HoverEvent.showText(...)) and clickEvent(ClickEvent.copyToClipboard(...)). Then append it after a gray "You are at " piece.
Show the solution
The command only works for players, because only players have a position. It builds the coordinates text once and uses it for both the visible component and the clipboard.
Where this file livestext-adventure-exercisesrcmainjavacomexampletextadventureexerciseWhereCommand.java
The package com.example.textadventureexercise is the folder path com/example/textadventureexercise inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- text-adventure-exercise/
- src/main/
- java/com/example/textadventureexercise/Package com.example.textadventureexercise
- TextExercisePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- WhereCommand.javayou are hereCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/textadventureexercise/Package com.example.textadventureexercise
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
15@Override16public void execute(CommandSourceStack source, String[] args) {17 if (!(source.getExecutor() instanceof Player player)) {18 source.getSender().sendMessage(Component.text("Only players have a position.", NamedTextColor.RED));19 return;20 }21 22 Location location = player.getLocation();23 String coordinates = location.getBlockX() + " " + location.getBlockY() + " " + location.getBlockZ();24 25 Component clickable = Component.text(coordinates, NamedTextColor.AQUA, TextDecoration.UNDERLINED)26 .hoverEvent(HoverEvent.showText(Component.text("Click to copy", NamedTextColor.GRAY)))27 .clickEvent(ClickEvent.copyToClipboard(coordinates));28 29 player.sendMessage(Component.text("You are at ", NamedTextColor.GRAY).append(clickable));30}- The coordinates as one text, for example
12 64 -30. We reuse it in two places. - Underlined text signals "you can click this".
- What lands in the clipboard.
Shift-click a name
Players can hold Shift and click chat text to paste it into their chat box. Find the Component method that sets that text, and use it to make a player's name in /who paste the name when shift-clicked.
Hint
Type .ins after a component in IntelliJ and read the autocomplete list.
Show the solution
The method is insertion(String). Add it to the name component in nameWithDetails:
1return Component.text(player.getName(), NamedTextColor.WHITE)2 .insertion(player.getName())3 .hoverEvent(HoverEvent.showText(details))4 .clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));A normal click still starts a private message, and a shift-click pastes the bare name.
You can download the demo plugin as a Gradle project, or experiment with the other examples in the Compile Lab.
Recap
- Modern text is a Component: words plus a style, with child components that inherit the parent's style.
- Components are immutable. Every
color,decorateorappendcall returns a new component, so keep the result. - Use
NamedTextColorfor the 16 classic colors,TextColor.color(...)for any color, andTextDecorationfor bold, italic and friends. - Use
Component.text()as a builder for long messages, andComponent.newline()andComponent.join(...)to combine pieces. - Attach
hoverEventandclickEventto make text interactive. A hover tooltip tells the player what the click does. - Use
Component.translatable(...)for vanilla names so every player reads their own language. - Players, the console and the server are all audiences:
sendMessageworks on any of them. - Serializers convert between components, plain text, old
&codes and JSON.
Quick quiz
You run
message.color(NamedTextColor.RED);on its own line and then sendmessage. What does the player see?Components are immutable.color(...)returns a new red component, and the code threw that result away. Writemessage = message.color(...)instead.A parent component is gold. Its child has no color of its own. What color is the child?
Children copy the parent's style unless they set their own. That is whyComponent.text("!")after a gold piece is gold too.Which click event types text into the player's chat box without sending it?
runCommandruns the command at once.copyToClipboardcopies without typing anything.suggestCommandonly fills in the chat box, so the player can finish it.Why is
Component.translatable(Material.DIAMOND_SWORD)better thanComponent.text(Material.DIAMOND_SWORD.name())for a message?name()returns the code name, which is not meant for players. A translatable component sends only a key and lets each player's game fill in the right word.You want to write a component to the server log without any colors. Which tool do you use?
"Serialize" turns a component into text. The plain-text serializer keeps the words and drops every style. The legacy serializer would keep color codes in the text, anddeserializegoes the other way.
Next steps
- MiniMessage: easy formatted text: write all of this as simple tags, and fill in placeholders safely.
- Titles, action bars, boss bars and sounds: more things that take a component.
- Commands, part 1: how
/rulesand/whowere registered. - MiniMessage Studio: type MiniMessage and see the result live.