Building GUI menus
Clickable chest menus with buttons, pages and confirm screens that players cannot steal from.
Server menus are everywhere: shops, warps, kit selectors, settings screens. They look like magic, but a menu is only a chest window that your plugin fills with clickable items. On this page you will build menus from scratch, stop players from stealing the buttons, and add pages and a yes/no confirm screen.
How a chest menu works
There is no special "menu" feature in Minecraft. A plugin menu is made from three ordinary things you already know:
- An inventory that your plugin creates and fills with items. Each item is a button.
- A command or other trigger that opens that inventory on a player's screen, like opening a chest.
- A listener for
InventoryClickEventthat notices which button the player clicked and runs your code. It also stops players from taking the items out.
Slot numbers and the grid
A chest window is a grid of 9 columns. It can have 1 to 6 rows, so a menu has 9, 18, 27, 36, 45 or 54 slots. Slots are numbered from 0, left to right and top to bottom.
This little class turns rows and columns into slots and back. You will use the same arithmetic all the time when you place buttons:
Where this file livesgui-menus-snippetssrcmainjavacomexampleguimenussnippetsSlotMath.java
The package com.example.guimenussnippets is the folder path com/example/guimenussnippets 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.
- gui-menus-snippets/
- src/main/java/com/example/guimenussnippets/Package com.example.guimenussnippets
- MenuTypeWay.javaHelper class
- SlotMath.javayou are hereHelper class
- 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/java/com/example/guimenussnippets/Package com.example.guimenussnippets
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.
9static int slotOf(int row, int column) {10 return row * 9 + column;11}- Give it a row and a column (both counted from 0) and get the slot number.
- Each full row is 9 slots, so skip
rowrows, then movecolumnslots right. Row 2, column 4 is 2 * 9 + 4 = 22.
Where this file livesgui-menus-snippetssrcmainjavacomexampleguimenussnippetsSlotMath.java
The package com.example.guimenussnippets is the folder path com/example/guimenussnippets 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.
- gui-menus-snippets/
- src/main/java/com/example/guimenussnippets/Package com.example.guimenussnippets
- MenuTypeWay.javaHelper class
- SlotMath.javayou are hereHelper class
- 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/java/com/example/guimenussnippets/Package com.example.guimenussnippets
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.
13static int rowOf(int slot) {14 return slot / 9;15}- Whole-number division throws away the remainder: slot 22 divided by 9 is 2, so it is in row 2.
Where this file livesgui-menus-snippetssrcmainjavacomexampleguimenussnippetsSlotMath.java
The package com.example.guimenussnippets is the folder path com/example/guimenussnippets 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.
- gui-menus-snippets/
- src/main/java/com/example/guimenussnippets/Package com.example.guimenussnippets
- MenuTypeWay.javaHelper class
- SlotMath.javayou are hereHelper class
- 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/java/com/example/guimenussnippets/Package com.example.guimenussnippets
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.
17static int columnOf(int slot) {18 return slot % 9;19}- The remainder of the same division: slot 22 leaves 4, so it is in column 4. Operators and math covers
/and%.
Here is a use of it that draws a frame of glass panes around the edge of any menu:
Where this file livesgui-menus-snippetssrcmainjavacomexampleguimenussnippetsSlotMath.java
The package com.example.guimenussnippets is the folder path com/example/guimenussnippets 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.
- gui-menus-snippets/
- src/main/java/com/example/guimenussnippets/Package com.example.guimenussnippets
- MenuTypeWay.javaHelper class
- SlotMath.javayou are hereHelper class
- 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/java/com/example/guimenussnippets/Package com.example.guimenussnippets
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.
21static void frame(Inventory menu) {22 ItemStack pane = ItemStack.of(Material.BLACK_STAINED_GLASS_PANE);23 int rows = menu.getSize() / 9;24 for (int slot = 0; slot < menu.getSize(); slot++) {25 boolean topOrBottom = rowOf(slot) == 0 || rowOf(slot) == rows - 1;26 boolean leftOrRight = columnOf(slot) == 0 || columnOf(slot) == 8;27 if (topOrBottom || leftOrRight) {28 menu.setItem(slot, pane);29 }30 }31}- How many rows the menu has.
- Visits every slot of the menu, from 0 to the last one.
- True for the top row and the bottom row. The two bars
||mean "or". - True for the first and last column.
- Puts a pane in every edge slot, leaving the middle free for buttons.
Your first menu
The smallest working menu has three small parts. Let us build it step by step: a menu with one diamond that says "You clicked the diamond!" when pressed.
A class that owns the inventory
It creates the inventory and puts the diamond in. It is called an inventory holder, and its job is to give your menu a name tag. Later, the listener can ask "does this inventory belong to my menu?".
A listener that handles clicks
It ignores every inventory that is not your menu, cancels the click so nothing can be taken, and reacts when the diamond's slot is clicked.
A command that opens it
For example
/hellomenu. Without a way to open it, nobody can see the menu.
Step 1: the holder
Where this file livesgui-menus-firstsrcmainjavacomexampleguimenusfirstHelloMenu.java
The package com.example.guimenusfirst is the folder path com/example/guimenusfirst 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.
- gui-menus-first/
- src/main/
- java/com/example/guimenusfirst/Package com.example.guimenusfirst
- FirstMenuPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- HelloMenu.javayou are hereMenu (inventory holder)
- HelloMenuListener.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/guimenusfirst/Package com.example.guimenusfirst
- 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.guimenusfirst;2 3import net.kyori.adventure.text.Component;4import net.kyori.adventure.text.minimessage.MiniMessage;5import org.bukkit.Bukkit;6import org.bukkit.Material;7import org.bukkit.inventory.Inventory;8import org.bukkit.inventory.InventoryHolder;9import org.bukkit.inventory.ItemStack;10 11final class HelloMenu implements InventoryHolder {12 13 static final int DIAMOND_SLOT = 13;14 15 private final Inventory inventory;16 17 HelloMenu() {18 this.inventory = Bukkit.createInventory(this, 27, Component.text("My first menu"));19 ItemStack diamond = ItemStack.of(Material.DIAMOND);20 diamond.editMeta(meta -> meta.displayName(MiniMessage.miniMessage().deserialize("<!italic><aqua>Click me!")));21 inventory.setItem(DIAMOND_SLOT, diamond);22 }23 24 @Override25 public Inventory getInventory() {26 return inventory;27 }28}- Marks this class as something that can own an inventory. It must have a
getInventorymethod. - The diamond's slot, saved under a name so the menu and the listener agree. Slot 13 is the middle of a 3-row menu: row 1, column 4.
- Makes a new empty inventory with 27 slots (3 rows). The first argument
thissays who owns it. The text is the window title. - The title is a
component (formatted text), like everything text-like in modern Paper. - Gives the diamond a name.
<!italic>switches the default italics off, as the Items page explains. - Places the diamond in the menu.
Step 2: the listener
Where this file livesgui-menus-firstsrcmainjavacomexampleguimenusfirstHelloMenuListener.java
The package com.example.guimenusfirst is the folder path com/example/guimenusfirst 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.
- gui-menus-first/
- src/main/
- java/com/example/guimenusfirst/Package com.example.guimenusfirst
- FirstMenuPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- HelloMenu.javaMenu (inventory holder)
- HelloMenuListener.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/guimenusfirst/Package com.example.guimenusfirst
- 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.
11@EventHandler12public void onClick(InventoryClickEvent event) {13 if (!(event.getInventory().getHolder(false) instanceof HelloMenu)) {14 return;15 }16 event.setCancelled(true);17 if (event.getRawSlot() == HelloMenu.DIAMOND_SLOT && event.getWhoClicked() instanceof Player player) {18 player.sendRichMessage("<green>You clicked the diamond!");19 player.closeInventory();20 }21}- Is the inventory being clicked owned by a
HelloMenu? If not, it is a chest or the player's inventory and we must not touch it. Thefalseonly means "do not copy block data". - The most important line of every menu. It stops the click from doing anything, so the diamond cannot be picked up, moved or duplicated.
- Which slot was clicked? The raw slot is unique for the whole window, and the menu is on top, so raw slot 13 is slot 13 of the menu.
- The clicker comes as a general
HumanEntity. This check makes sure it is a player, and names itplayer. - Closes the window, like pressing Esc.
Where this file livesgui-menus-firstsrcmainjavacomexampleguimenusfirstHelloMenuListener.java
The package com.example.guimenusfirst is the folder path com/example/guimenusfirst 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.
- gui-menus-first/
- src/main/
- java/com/example/guimenusfirst/Package com.example.guimenusfirst
- FirstMenuPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- HelloMenu.javaMenu (inventory holder)
- HelloMenuListener.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/guimenusfirst/Package com.example.guimenusfirst
- 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.
23@EventHandler24public void onDrag(InventoryDragEvent event) {25 if (event.getInventory().getHolder(false) instanceof HelloMenu) {26 event.setCancelled(true);27 }28}- Dragging an item across several slots is a different event from clicking, so it needs its own handler. Cancel it too.
Step 3: the command
Where this file livesgui-menus-firstsrcmainjavacomexampleguimenusfirstFirstMenuPlugin.java
The package com.example.guimenusfirst is the folder path com/example/guimenusfirst 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.
- gui-menus-first/
- src/main/
- java/com/example/guimenusfirst/Package com.example.guimenusfirst
- FirstMenuPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- HelloMenu.javaMenu (inventory holder)
- HelloMenuListener.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/guimenusfirst/Package com.example.guimenusfirst
- 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.guimenusfirst;2 3import io.papermc.paper.command.brigadier.BasicCommand;4import io.papermc.paper.command.brigadier.CommandSourceStack;5import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;6import org.bukkit.entity.Player;7import org.bukkit.plugin.java.JavaPlugin;8 9public final class FirstMenuPlugin extends JavaPlugin {10 11 @Override12 public void onEnable() {13 getServer().getPluginManager().registerEvents(new HelloMenuListener(), this);14 getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event ->15 event.registrar().register("hellomenu", "Open a tiny menu", new BasicCommand() {16 @Override17 public void execute(CommandSourceStack source, String[] args) {18 if (source.getExecutor() instanceof Player player) {19 player.openInventory(new HelloMenu().getInventory());20 }21 }22 }));23 }24}- Do not forget to register the listener, or clicks are never canceled and the diamond can be stolen.
- A tiny command written inline. Commands, part 1 explains how
BasicCommandworks. - Creates a brand-new menu for this player and shows it.
Where this file livesgui-menus-firstsrcmainresourcesplugin.yml
Files in src/main/resources are copied into the jar exactly as they are. Paper looks for plugin.yml at the top of the jar.
- gui-menus-first/
- src/main/
- java/com/example/guimenusfirst/Package com.example.guimenusfirst
- FirstMenuPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- HelloMenu.javaMenu (inventory holder)
- HelloMenuListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlyou are hereTells Paper the plugin's name, version and main class
- java/com/example/guimenusfirst/Package com.example.guimenusfirst
- 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.
1name: GuiMenusFirst2version: '1.0.0'3main: com.example.guimenusfirst.FirstMenuPlugin4api-version: '26.3'5description: The smallest possible clickable chest menu, opened with /hellomenu.- Points at the plugin's main class.
Type /hellomenu and you see this window. Click the diamond and the chat tells you, while the window closes:
Keeping the items safe
Players are creative at breaking menus. If you only cancel the obvious left click, they will find ways around it. Everything in this table is a click in a menu window, and each one must end up canceled:
| What the player does | What it would do without protection | Event |
|---|---|---|
| Left or right click a button | Picks the item up | InventoryClickEvent |
| Shift-click an item in their own inventory | Moves it into the menu | InventoryClickEvent (clicked inventory is the bottom one) |
| Shift-click a button | Moves it into their inventory | InventoryClickEvent |
| Press a number key over a button | Swaps the button into the hotbar | InventoryClickEvent (NUMBER_KEY) |
| Double-click to collect matching items | Pulls every matching button onto the cursor | InventoryClickEvent (DOUBLE_CLICK) |
| Press Q over a button | Drops the button on the floor | InventoryClickEvent (DROP) |
| Hold a click and drag across slots | Spreads items into the menu | InventoryDragEvent |
The first six are all click events, so one rule covers them: cancel every click while the open window belongs to your menu, whichever of the two inventories was clicked. Then handle the buttons only when the click was in the menu part. The drag has its own event and its own cancel.
1@EventHandler2public void onClick(InventoryClickEvent event) {3 if (event.getClickedInventory() == event.getView().getTopInventory()) {4 event.setCancelled(true);5 }6}- Only cancels clicks in the menu itself. A shift-click from the player's own inventory is a click in the bottom inventory, so it is not canceled, and it puts items into the menu.
- Cancels in the wrong place. Cancel for the whole window, then decide what to run.
Notice that the working listener from earlier checks the holder of the whole window, not which part was clicked. That is the fix.
A menu base class
The first menu works, but one listener per menu does not scale: a plugin with ten menus needs ten listeners. Real plugins write a small base class that does the common work, so each new menu only says what its buttons look like and what they do. This section builds the one used in Paper's own example project, example-gui from the paper-templates folder.
The base class is called Menu. It has four jobs: own an inventory, remember what each slot does, redraw on demand, and run the right action when a slot is clicked.
Where this file livesgui-menussrcmainjavacomexampleguimenusMenu.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javayou are hereMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
15public abstract class Menu implements InventoryHolder {16 17 private final Inventory inventory;18 private final Map<Integer, Consumer<InventoryClickEvent>> actions = new HashMap<>();19 20 protected Menu(int rows, Component title) {21 this.inventory = Bukkit.createInventory(this, rows * 9, title);22 }- An
abstract class cannot be created directly; it exists to be extended by real menus. It is also the holder, so it is the name tag for every menu. - The actual window contents. It never changes to a different inventory.
- The key part. A
map from slot number to "what to do when this slot is clicked".Consumeris a piece of code that takes the click event. - Subclasses say how many rows and what title they want.
- The size of the inventory is rows times 9.
Where this file livesgui-menussrcmainjavacomexampleguimenusMenu.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javayou are hereMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
24protected abstract void render(Player viewer);- Every menu must write its own
render, which fills the inventory with buttons for one viewer.abstractmeans "no body here, children must supply it".
Where this file livesgui-menussrcmainjavacomexampleguimenusMenu.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javayou are hereMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
26public final void open(Player player) {27 redraw(player);28 player.openInventory(inventory);29}finalmeans no subclass may replace this method, so every menu opens the same way.- Always draws a fresh set of buttons just before showing the window, so what the player sees is current.
- Shows the window.
Where this file livesgui-menussrcmainjavacomexampleguimenusMenu.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javayou are hereMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
31protected final void redraw(Player viewer) {32 inventory.clear();33 actions.clear();34 render(viewer);35}- Removes every old button.
- Forgets the old click actions too. Otherwise a button that no longer exists could still run code.
- Asks the real menu to draw its buttons again.
Where this file livesgui-menussrcmainjavacomexampleguimenusMenu.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javayou are hereMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
37protected final void button(int slot, ItemStack icon, Consumer<InventoryClickEvent> action) {38 inventory.setItem(slot, icon);39 actions.put(slot, action);40}- What the button looks like.
- What happens on a click. Callers pass it as a
lambda :event -> viewer.closeInventory(). - Remembers "this slot runs this code".
Where this file livesgui-menussrcmainjavacomexampleguimenusMenu.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javayou are hereMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
42protected final void icon(int slot, ItemStack icon) {43 inventory.setItem(slot, icon);44 actions.remove(slot);45}- An icon is a button that does nothing, like a label or a decoration. It makes sure no old action is left behind.
Where this file livesgui-menussrcmainjavacomexampleguimenusMenu.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javayou are hereMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
63final void handleClick(InventoryClickEvent event) {64 Consumer<InventoryClickEvent> action = actions.get(event.getSlot());65 if (action != null && event.getWhoClicked() instanceof Player player) {66 click(player);67 action.accept(event);68 }69}- Finds what this slot should do. The result is
nullwhen nothing is registered, such as for a filler pane. - Only runs for slots that have an action.
- Plays a click sound, so buttons feel like buttons.
- Runs the button's code.
One small listener serves every menu. Its job is exactly what you learned in the table above:
Where this file livesgui-menussrcmainjavacomexampleguimenusMenuListener.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javayou are hereListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
11@EventHandler(priority = EventPriority.HIGH)12public void onClick(InventoryClickEvent event) {13 if (!(event.getInventory().getHolder(false) instanceof Menu menu)) {14 return;15 }16 event.setCancelled(true);17 if (event.getClickedInventory() == event.getView().getTopInventory()) {18 menu.handleClick(event);19 }20}- Runs later than normal-priority listeners, so another plugin that un-cancels the event does not undo ours. See Event priorities and canceling.
- Is this window one of our menus? If so, name it
menu. - Cancels every click in a menu window, top or bottom.
- Only clicks on the menu part reach the buttons. Clicks on the player's own inventory are canceled but ignored.
- Looks up and runs the button's action.
Where this file livesgui-menussrcmainjavacomexampleguimenusMenuListener.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javayou are hereListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
22@EventHandler(priority = EventPriority.HIGH)23public void onDrag(InventoryDragEvent event) {24 if (event.getInventory().getHolder(false) instanceof Menu) {25 event.setCancelled(true);26 }27}- Drags in any menu are canceled.
Making buttons
Most buttons need the same few things: a material, a colored name and some lore. A tiny helper class keeps menus short:
Where this file livesgui-menussrcmainjavacomexampleguimenusMenuItems.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuItems.javayou are hereHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
19public static ItemStack button(Material material, String name, String... lore) {20 ItemStack item = ItemStack.of(material);21 item.editMeta(meta -> {22 meta.displayName(text(name));23 meta.lore(Arrays.stream(lore).map(MenuItems::text).toList());24 });25 return item;26}Where this file livesgui-menussrcmainjavacomexampleguimenusMenuItems.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuItems.javayou are hereHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
34public static ItemStack filler() {35 return button(Material.GRAY_STAINED_GLASS_PANE, " ");36}- A filler pane is a glass pane with a blank name. It fills unused slots so the menu looks tidy. Its name is a single space, so hovering it shows almost nothing.
Where this file livesgui-menussrcmainjavacomexampleguimenusMenuItems.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuItems.javayou are hereHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
38public static Component text(String miniMessage) {39 return MINI.deserialize(miniMessage).decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE);40}- Turns tag text like
<gold>Healinto a component. - Turns italics off, unless the text itself asked for italics. This is why button names are not slanted.
A real menu: the main menu
With the base class ready, a new menu is just a class that fills its slots. Compare how little code a button needs:
Where this file livesgui-menussrcmainjavacomexampleguimenusMainMenu.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javayou are hereHelper class
- Menu.javaMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
11public MainMenu() {12 super(3, MenuItems.text("<dark_purple><bold>Example Menu"));13}14 15@Override16protected void render(Player viewer) {17 boolean admin = viewer.hasPermission("guimenus.admin");18 19 button(10, MenuItems.button(Material.GOLDEN_APPLE, "<gold>Heal", "<gray>Restore your health and hunger."), event -> {20 AttributeInstance maxHealth = viewer.getAttribute(Attribute.MAX_HEALTH);21 viewer.setHealth(maxHealth == null ? 20.0 : maxHealth.getValue());22 viewer.setFoodLevel(20);23 viewer.sendRichMessage("<green>You feel refreshed.");24 });- The word
supercalls the constructor of the parent class,Menu. Here it asks for three rows and a purple bold title. - Fills in the buttons. The viewer is passed in, so buttons can differ between players.
- Is this viewer an admin? It decides whether the admin button shows up further down.
- A button in slot 10, drawn as a golden apple named Heal.
- The click action. It runs only when slot 10 is clicked.
- The player's maximum health. It could be missing, so the next line falls back to 20.
- Fills the hunger bar.
Where this file livesgui-menussrcmainjavacomexampleguimenusMainMenu.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javayou are hereHelper class
- Menu.javaMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
39 if (admin) {40 World world = viewer.getWorld();41 button(16, MenuItems.button(Material.CLOCK, "<yellow>Toggle Day and Night", "<gray>Current time: <white>" + world.getTime()), event -> {42 world.setTime(world.isDayTime() ? 13000 : 1000);43 redraw(viewer);44 });45 }46 47 button(22, MenuItems.button(Material.BARRIER, "<red>Close"), event -> viewer.closeInventory());48 fillEmpty(MenuItems.filler());49}- This button exists only for players with the admin permission. Everyone else sees a filler pane there.
- Refreshes the menu while it is open. Here it updates the "current time" line in the lore after the click, so the player sees the change instantly.
- A conventional close button. Closing is a single call.
- Fill every slot that has no button with a filler pane. Always call it last.
This is what /menu shows an administrator. Regular players do not get the clock:
Updating a menu while it is open
Calling redraw(viewer) clears the inventory and calls render again. The viewer's window updates immediately, because it is looking at that same inventory object. You can use it for counters, toggles and anything else that changes while the menu stays open. If only one slot changes, inventory.setItem(slot, newItem) is enough.
Paged menus
A chest holds at most 54 items. A list of online players can be longer than that, so you split it into pages and add Previous and Next buttons. Here is the menu that shows online players as their heads. The menu keeps the current page number as a field:
Where this file livesgui-menussrcmainjavacomexampleguimenusPlayerListMenu.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javayou are hereHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
12private static final int ROWS = 6;13private static final int PER_PAGE = (ROWS - 1) * 9;14 15private int page;16 17public PlayerListMenu(int page) {18 super(ROWS, MenuItems.text("<dark_aqua><bold>Online Players"));19 this.page = page;20}- Six rows is the biggest menu.
- The top five rows, 45 slots, hold the players. The bottom row is for navigation buttons.
- The page the viewer is on, counted from 0. A normal field, so the menu remembers it between clicks.
Where this file livesgui-menussrcmainjavacomexampleguimenusPlayerListMenu.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javayou are hereHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
22@Override23protected void render(Player viewer) {24 List<Player> players = new ArrayList<>(Bukkit.getOnlinePlayers());25 players.sort(Comparator.comparing(Player::getName, String.CASE_INSENSITIVE_ORDER));26 int pages = Math.max(1, (players.size() + PER_PAGE - 1) / PER_PAGE);27 page = Math.clamp(page, 0, pages - 1);28 boolean canTeleport = viewer.hasPermission("guimenus.admin");29 30 List<Player> visible = players.subList(page * PER_PAGE, Math.min(players.size(), (page + 1) * PER_PAGE));31 for (int index = 0; index < visible.size(); index++) {32 Player target = visible.get(index);33 String world = "<gray>World: <white>" + target.getWorld().getName();34 String ping = "<gray>Ping: <white>" + target.getPing() + " ms";35 String[] lore = canTeleport ? new String[] {world, ping, "<yellow>Click to teleport"} : new String[] {world, ping};36 button(index, MenuItems.head(target, "<aqua>" + target.getName(), lore), event -> {37 if (canTeleport && target.isOnline()) {38 viewer.teleportAsync(target.getLocation());39 viewer.closeInventory();40 }41 });42 }- Copies the online players into a list we can sort.
- Sorts by name, ignoring upper and lower case, so the order is stable between redraws.
- The number of pages, rounded up. With 46 players you need 2 pages, and this formula gives (46 + 44) / 45 = 2. A plain division would round down and lose the last page.
- Keeps the page number in range, even if players left while the menu was open.
- Takes just the players for this page, for example players 45 to 89 for the second page.
- Draws the player's own skin as the button icon, using a player head item.
- Admins get an extra lore line telling them the button teleports.
Where this file livesgui-menussrcmainjavacomexampleguimenusPlayerListMenu.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javayou are hereHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
44int navigationRow = size() - 9;45button(navigationRow, MenuItems.button(Material.OAK_DOOR, "<gray>Back"), event -> new MainMenu().open(viewer));46if (page > 0) {47 button(navigationRow + 3, MenuItems.button(Material.ARROW, "<yellow>Previous Page"), event -> {48 page--;49 redraw(viewer);50 });51}52icon(navigationRow + 4, MenuItems.button(Material.PAPER, "<white>Page " + (page + 1) + " of " + pages,53 "<gray>" + players.size() + " player(s) online"));54if (page < pages - 1) {55 button(navigationRow + 5, MenuItems.button(Material.ARROW, "<yellow>Next Page"), event -> {56 page++;57 redraw(viewer);58 });59}60button(navigationRow + 8, MenuItems.button(Material.SUNFLOWER, "<green>Refresh"), event -> redraw(viewer));61 62for (int slot = navigationRow; slot < size(); slot++) {63 if (getInventory().getItem(slot) == null) {64 icon(slot, MenuItems.filler());65 }66}- The first slot of the last row. All the navigation buttons live there.
- Back button: opens the main menu again, which makes a menu a small tree of screens.
- Only show Previous when there is a previous page.
- Goes to the previous page...
- ...and redraws, so the window now shows the new page.
- An icon, not a button: it shows "Page 1 of 3" and does nothing when clicked.
- Only show Next when there is a next page.
This is page 2 of a server with 118 players online, as a regular player sees it (an admin would also get a "Click to teleport" line on every head):
A confirm menu
Dangerous actions, such as clearing an inventory or buying something expensive, deserve an "Are you sure?" screen. The confirm menu is a tiny menu with a Confirm and a Cancel button. The clever part: it does not know what it is confirming. The caller gives it two pieces of code, one for each outcome:
Where this file livesgui-menussrcmainjavacomexampleguimenusConfirmMenu.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javayou are hereHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.guimenus;2 3import org.bukkit.Material;4import org.bukkit.entity.Player;5 6public final class ConfirmMenu extends Menu {7 8 private final Runnable onConfirm;9 private final Runnable onCancel;10 11 public ConfirmMenu(String question, Runnable onConfirm, Runnable onCancel) {12 super(3, MenuItems.text(question));13 this.onConfirm = onConfirm;14 this.onCancel = onCancel;15 }16 17 @Override18 protected void render(Player viewer) {19 button(11, MenuItems.button(Material.LIME_CONCRETE, "<green><bold>Confirm"), event -> onConfirm.run());20 button(15, MenuItems.button(Material.RED_CONCRETE, "<red><bold>Cancel"), event -> onCancel.run());21 fillEmpty(MenuItems.filler());22 }23}- A
Runnableis a piece of code with no inputs and no result that you can run later by callingrun(). It holds "what to do on yes". - The question, such as "Clear your inventory?", becomes the window title.
- Green means yes, red means no. The colors tell players what to expect without reading.
- Clicking Confirm runs the code the caller gave.
And this is how the main menu uses it for its Clear Inventory button. It is the part you copy for your own dangerous buttons:
Where this file livesgui-menussrcmainjavacomexampleguimenusMainMenu.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javayou are hereHelper class
- Menu.javaMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
29button(14, MenuItems.button(Material.LAVA_BUCKET, "<red>Clear Inventory", "<gray>Empties your inventory.", "<dark_gray>Asks for confirmation first"),30 event -> new ConfirmMenu(31 "<red>Clear your inventory?",32 () -> {33 viewer.getInventory().clear();34 viewer.sendRichMessage("<gray>Your inventory was cleared.");35 viewer.closeInventory();36 },37 () -> open(viewer)).open(viewer));- Opens a confirm screen when the lava bucket is clicked.
- The "yes" code: clear the inventory, tell the player and close the window.
- The dangerous action itself. It only runs after Confirm was clicked.
- The "no" code: simply go back to the main menu.
The plugin that ties it together
- gui-menus/
- src/
- main/
- java/
- com/example/guimenus/
- GuiMenusPlugin.javaRegisters the listener and the /menu command
- Menu.javaThe base class: holder, actions per slot, open and redraw
- MenuListener.javaCancels clicks and drags and runs button actions
- MenuItems.javaHelpers for buttons, heads and filler panes
- MainMenu.javaThe first screen with Heal, Online Players, Clear and Close
- PlayerListMenu.javaThe paged list of online players
- ConfirmMenu.javaThe yes/no screen
- com/example/guimenus/
- resources/
- plugin.ymlName, main class and the guimenus.menu and guimenus.admin permissions
- java/
- main/
- src/
Where this file livesgui-menussrcmainjavacomexampleguimenusGuiMenusPlugin.java
The package com.example.guimenus is the folder path com/example/guimenus 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.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.guimenus;2 3import io.papermc.paper.command.brigadier.BasicCommand;4import io.papermc.paper.command.brigadier.CommandSourceStack;5import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;6import org.bukkit.entity.Player;7import org.bukkit.plugin.java.JavaPlugin;8 9public final class GuiMenusPlugin extends JavaPlugin {10 11 @Override12 public void onEnable() {13 getServer().getPluginManager().registerEvents(new MenuListener(), this);14 getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event ->15 event.registrar().register("menu", "Open the example menu", new MenuCommand()));16 }17 18 private static final class MenuCommand implements BasicCommand {19 20 @Override21 public void execute(CommandSourceStack source, String[] args) {22 if (source.getExecutor() instanceof Player player) {23 new MainMenu().open(player);24 } else {25 source.getSender().sendRichMessage("<red>Only players can open menus.");26 }27 }28 29 @Override30 public String permission() {31 return "guimenus.menu";32 }33 }34}- One listener for all menus. Add new menus as classes, no new listener needed.
- The command lives inside the plugin class because it is tiny. A nested class is a class written inside another one.
- The whole command: create the menu, open it.
- Who may type
/menu.
Where this file livesgui-menussrcmainresourcesplugin.yml
Files in src/main/resources are copied into the jar exactly as they are. Paper looks for plugin.yml at the top of the jar.
- gui-menus/
- src/main/
- java/com/example/guimenus/Package com.example.guimenus
- ConfirmMenu.javaHelper class
- GuiMenusPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlyou are hereTells Paper the plugin's name, version and main class
- java/com/example/guimenus/Package com.example.guimenus
- 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.
1name: GuiMenus2version: '1.0.0'3main: com.example.guimenus.GuiMenusPlugin4api-version: '26.3'5description: Chest menus with buttons, pages, player heads and a confirm screen.6permissions:7 guimenus.menu:8 description: Lets a player open the example menu.9 default: true10 guimenus.admin:11 description: Shows the admin buttons and lets a player teleport from the player list.12 default: op- Only operators have this by default. It controls the clock button and the teleport buttons.
You can download this plugin as a Gradle project, run it and press /menu, or open it in the Compile Lab.
createInventory or MenuType?
The examples use Bukkit.createInventory(holder, size, title). It has been the standard way for years, it works on every server and every tutorial uses it, and the holder trick gives you reliable menu recognition. Paper also offers a newer way, MenuType, which builds an InventoryView, the whole window, for a specific player:
Where this file livesgui-menus-snippetssrcmainjavacomexampleguimenussnippetsMenuTypeWay.java
The package com.example.guimenussnippets is the folder path com/example/guimenussnippets 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.
- gui-menus-snippets/
- src/main/java/com/example/guimenussnippets/Package com.example.guimenussnippets
- MenuTypeWay.javayou are hereHelper class
- SlotMath.javaHelper class
- 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/java/com/example/guimenussnippets/Package com.example.guimenussnippets
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.
20void openWithMenuType(Player player) {21 InventoryView view = MenuType.GENERIC_9X3.create(player, Component.text("MenuType menu"));22 view.getTopInventory().setItem(13, ItemStack.of(Material.EMERALD));23 view.open();24}- A chest window with 9 columns and 3 rows. There are also
GENERIC_9X1toGENERIC_9X6, plus hoppers, anvils, furnaces and more. - Creates the view for that player with a title. It does not show it yet.
- The inventory you fill with buttons.
- Shows the window.
Where this file livesgui-menus-snippetssrcmainjavacomexampleguimenussnippetsMenuTypeWay.java
The package com.example.guimenussnippets is the folder path com/example/guimenussnippets 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.
- gui-menus-snippets/
- src/main/java/com/example/guimenussnippets/Package com.example.guimenussnippets
- MenuTypeWay.javayou are hereHelper class
- SlotMath.javaHelper class
- 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/java/com/example/guimenussnippets/Package com.example.guimenussnippets
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.
14void openWithCreateInventory(Player player) {15 Inventory menu = Bukkit.createInventory(null, 27, Component.text("Classic menu"));16 menu.setItem(13, ItemStack.of(Material.EMERALD));17 player.openInventory(menu);18}- The classic way. Passing
nullas the holder means no owner, so you cannot recognize this menu later. That is fine for a throwaway window, and the reason the examples use a holder.
For a beginner, stay with createInventory and a holder. Reach for MenuType when you need window types other than chests, or features that belong to a view, such as changing the title while the window is open.
Mistakes to avoid
- Forgetting to cancel clicks. The result is free items for everyone. Cancel first, then decide what to do.
- Canceling only clicks in the top inventory. Shift-clicks from the player's own inventory are clicks in the bottom inventory and would slip in.
- Forgetting the drag event. It is a separate event. Cancel it too.
- Recognizing a menu by its title. Players can fake titles. Use a holder.
- Not registering the listener. The menu opens and looks fine, but nothing responds, and nothing is protected.
- Using
getSlot()without checking the inventory. The same slot number exists in the player's inventory. CheckgetClickedInventory()first, or usegetRawSlot()when the menu is on top. - Doing slow work in a click handler. Clicks run on the main thread. Reading files or databases there makes the whole server lag. See Threads, performance and lag.
- Forgetting
redraw. If a button changes some state (a toggle, a counter), the item in the window does not change by itself.
Add a Feed button
Add a button to the main menu that fills the player's hunger bar and tells them "You feel full.". Use cooked beef as its icon.
Hint 1
Copy the Heal button and change the icon, the name and what the action does. Pick an empty slot such as 11.
Hint 2
viewer.setFoodLevel(20) fills hunger.
Show the solution
One new button call inside render is all it takes. The base class does the rest: it places the item, remembers the action and cancels the clicks.
Where this file livesgui-menus-exercisesrcmainjavacomexampleguimenusexerciseMainMenu.java
The package com.example.guimenusexercise is the folder path com/example/guimenusexercise 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.
- gui-menus-exercise/
- src/main/
- java/com/example/guimenusexercise/Package com.example.guimenusexercise
- ConfirmMenu.javaHelper class
- GuiMenusExercisePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javayou are hereHelper class
- Menu.javaMenu (inventory holder)
- MenuCenter.javaHelper class
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenusexercise/Package com.example.guimenusexercise
- 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.
26button(11, MenuItems.button(Material.COOKED_BEEF, "<gold>Feed", "<gray>Fill your hunger bar."), event -> {27 viewer.setFoodLevel(20);28 viewer.sendRichMessage("<green>You feel full.");29});- Slot 11, between Heal and Online Players.
- The one-line action.
Find the middle
Write static int centerSlot(int rows), which returns the slot in the center of a menu: row (rows - 1) / 2, column 4. For a 3-row menu it must return 13, and for a 5-row menu it must return 22.
Hint 1
Use the formula row * 9 + column from the start of this page.
Hint 2
(3 - 1) / 2 is 1, so the row is 1 and the slot is 1 * 9 + 4 = 13.
Show the solution
Work out the middle row, then turn row and column into a slot. For even row counts the whole-number division picks the upper of the two middle rows.
Where this file livesgui-menus-exercisesrcmainjavacomexampleguimenusexerciseMenuCenter.java
The package com.example.guimenusexercise is the folder path com/example/guimenusexercise 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.
- gui-menus-exercise/
- src/main/
- java/com/example/guimenusexercise/Package com.example.guimenusexercise
- ConfirmMenu.javaHelper class
- GuiMenusExercisePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MainMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuCenter.javayou are hereHelper class
- MenuItems.javaHelper class
- MenuListener.javaListener: reacts to events
- PlayerListMenu.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/guimenusexercise/Package com.example.guimenusexercise
- 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.
8static int centerSlot(int rows) {9 return (rows - 1) / 2 * 9 + 4;10}- The middle row, counted from 0. Division drops the remainder, so 4 / 2 is 2 and 5 / 2 would also be 2.
- Row times 9, plus column 4, the middle column of 0 to 8.
Recap
- A menu is an inventory you create with
Bukkit.createInventory(holder, rows * 9, title), fill with buttons and open withplayer.openInventory(...). - Slots are numbered from 0, left to right, top to bottom: slot = row * 9 + column.
- An
InventoryHolderclass is the safe way to recognize your own menus. Never rely on the title. - Cancel every
InventoryClickEventin a menu window, in both inventories, and cancelInventoryDragEventtoo. Handle button actions only for clicks in the top inventory. - A
Menubase class with a map from slot to action keeps each new menu short. Filler panes tidy the gaps, andredrawupdates the open window. - Paged menus compute pages with a rounded-up division,
subListand Previous and Next buttons. A confirm menu takes the "yes" and "no" code from its caller.
Quick quiz
Which slot is in row 2, column 4 of a chest menu (both counted from 0)?
Slot = row * 9 + column = 2 * 9 + 4 = 22. Slot 18 is row 2, column 0, and slot 24 is row 2, column 6.Why should the click listener cancel clicks in the player's own (bottom) inventory too, while a menu is open?
Shift-clicking, number keys and double-clicks can move items between the two inventories even when the click lands in the bottom one. Canceling the whole window closes those loopholes.How does the example recognize that an inventory belongs to its menu?
Titles and sizes are easy to imitate. A holder object that only your plugin creates cannot be faked by a player.A paged menu shows 45 players per page and 46 players are online. What does
(46 + 45 - 1) / 45give, and why use it?Whole-number division drops the remainder, so 46 / 45 would be 1 and the last player would have no page. Adding 44 first rounds up to 2.What is a
Runnableused for in the confirm menu?Passing code as a value lets one confirm menu serve every dangerous action. The caller decides what yes and no mean.
Next steps
- Project: Shop menu: build a real shop out of what you learned here.
- Saving data on things: store extra information on a button item.
- Timing and tasks: update a menu every second, like a countdown.
- Inventories: back to slots, counting and events.