Paper Plugin Guide
File mode0

Paper essentials

Building GUI menus

Clickable chest menus with buttons, pages and confirm screens that players cannot steal from.

Intermediate64 min read

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:

  1. An inventory that your plugin creates and fills with items. Each item is a button.
  2. A command or other trigger that opens that inventory on a player's screen, like opening a chest.
  3. A listener for InventoryClickEvent that 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.

Chest slot numbers A large chest has 6 rows of 9 slots, numbered 0 to 53 from left to right and top to bottom. The slot number is the row times 9 plus the column, counting from 0. col 0 col 1 col 2 col 3 col 4 col 5 col 6 col 7 col 8 row 0 0 1 2 3 4 5 6 7 8 row 1 9 10 11 12 13 14 15 16 17 row 2 18 19 20 21 22 23 24 25 26 row 3 27 28 29 30 31 32 33 34 35 row 4 36 37 38 39 40 41 42 43 44 row 5 45 46 47 48 49 50 51 52 53 slot = row * 9 + column Row 2, column 4 is slot 2 * 9 + 4 = 22 (the purple one). Counting always starts at 0, so the first slot is 0 and the last is 53.
A large chest: the slot number is the row times 9 plus the column, counting rows and columns from 0.

This little class turns rows and columns into slots and back. You will use the same arithmetic all the time when you place buttons:

SlotMath.javaCompiles on Paper 26.3Compile Lab
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

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}
  1. Give it a row and a column (both counted from 0) and get the slot number.
  2. Each full row is 9 slots, so skip row rows, then move column slots right. Row 2, column 4 is 2 * 9 + 4 = 22.
SlotMath.javaCompiles on Paper 26.3Compile Lab
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

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}
  1. Whole-number division throws away the remainder: slot 22 divided by 9 is 2, so it is in row 2.
SlotMath.javaCompiles on Paper 26.3Compile Lab
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

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}
  1. 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:

SlotMath.javaCompiles on Paper 26.3Compile Lab
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

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}
  1. How many rows the menu has.
  2. Visits every slot of the menu, from 0 to the last one.
  3. True for the top row and the bottom row. The two bars || mean "or".
  4. True for the first and last column.
  5. 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.

  1. 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?".

  2. 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.

  3. A command that opens it

    For example /hellomenu. Without a way to open it, nobody can see the menu.

Step 1: the holder

HelloMenu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Marks this class as something that can own an inventory. It must have a getInventory method.
  2. 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.
  3. Makes a new empty inventory with 27 slots (3 rows). The first argument this says who owns it. The text is the window title.
  4. The title is a component (formatted text), like everything text-like in modern Paper.
  5. Gives the diamond a name. <!italic> switches the default italics off, as the Items page explains.
  6. Places the diamond in the menu.

Step 2: the listener

HelloMenuListener.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. 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. The false only means "do not copy block data".
  2. The most important line of every menu. It stops the click from doing anything, so the diamond cannot be picked up, moved or duplicated.
  3. 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.
  4. The clicker comes as a general HumanEntity. This check makes sure it is a player, and names it player.
  5. Closes the window, like pressing Esc.
HelloMenuListener.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. 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

FirstMenuPlugin.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Do not forget to register the listener, or clicks are never canceled and the diamond can be stolen.
  2. A tiny command written inline. Commands, part 1 explains how BasicCommand works.
  3. Creates a brand-new menu for this player and shows it.
plugin.ymlCompiles on Paper 26.3Compile Lab
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
    • 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

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.
  1. 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:

13: diamond
You clicked the diamond!

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 doesWhat it would do without protectionEvent
Left or right click a buttonPicks the item upInventoryClickEvent
Shift-click an item in their own inventoryMoves it into the menuInventoryClickEvent (clicked inventory is the bottom one)
Shift-click a buttonMoves it into their inventoryInventoryClickEvent
Press a number key over a buttonSwaps the button into the hotbarInventoryClickEvent (NUMBER_KEY)
Double-click to collect matching itemsPulls every matching button onto the cursorInventoryClickEvent (DOUBLE_CLICK)
Press Q over a buttonDrops the button on the floorInventoryClickEvent (DROP)
Hold a click and drag across slotsSpreads items into the menuInventoryDragEvent

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.

A menu that can be robbedHas a mistake
1@EventHandler2public void onClick(InventoryClickEvent event) {3    if (event.getClickedInventory() == event.getView().getTopInventory()) {4        event.setCancelled(true);5    }6}
  1. 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.
  2. 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.

Menu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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    }
  1. 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.
  2. The actual window contents. It never changes to a different inventory.
  3. The key part. A map from slot number to "what to do when this slot is clicked". Consumer is a piece of code that takes the click event.
  4. Subclasses say how many rows and what title they want.
  5. The size of the inventory is rows times 9.
Menu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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);
  1. Every menu must write its own render, which fills the inventory with buttons for one viewer. abstract means "no body here, children must supply it".
Menu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. final means no subclass may replace this method, so every menu opens the same way.
  2. Always draws a fresh set of buttons just before showing the window, so what the player sees is current.
  3. Shows the window.
Menu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Removes every old button.
  2. Forgets the old click actions too. Otherwise a button that no longer exists could still run code.
  3. Asks the real menu to draw its buttons again.
Menu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. What the button looks like.
  2. What happens on a click. Callers pass it as a lambda: event -> viewer.closeInventory().
  3. Remembers "this slot runs this code".
Menu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. An icon is a button that does nothing, like a label or a decoration. It makes sure no old action is left behind.
Menu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Finds what this slot should do. The result is null when nothing is registered, such as for a filler pane.
  2. Only runs for slots that have an action.
  3. Plays a click sound, so buttons feel like buttons.
  4. Runs the button's code.

One small listener serves every menu. Its job is exactly what you learned in the table above:

MenuListener.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Runs later than normal-priority listeners, so another plugin that un-cancels the event does not undo ours. See Event priorities and canceling.
  2. Is this window one of our menus? If so, name it menu.
  3. Cancels every click in a menu window, top or bottom.
  4. Only clicks on the menu part reach the buttons. Clicks on the player's own inventory are canceled but ignored.
  5. Looks up and runs the button's action.
MenuListener.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. 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:

MenuItems.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Three dots mean "any number of strings", so callers can pass zero, one or five lore lines.
  2. Changes the item's name and lore, as taught on the Items page.
  3. Turns every lore string into a component. Streams are covered in the Java chapters, and this line simply means "convert each one".
MenuItems.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. 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.
MenuItems.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Turns tag text like <gold>Heal into a component.
  2. 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:

MainMenu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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    });
  1. The word super calls the constructor of the parent class, Menu. Here it asks for three rows and a purple bold title.
  2. Fills in the buttons. The viewer is passed in, so buttons can differ between players.
  3. Is this viewer an admin? It decides whether the admin button shows up further down.
  4. A button in slot 10, drawn as a golden apple named Heal.
  5. The click action. It runs only when slot 10 is clicked.
  6. The player's maximum health. It could be missing, so the next line falls back to 20.
  7. Fills the hunger bar.
MainMenu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. This button exists only for players with the admin permission. Everyone else sees a filler pane there.
  2. 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.
  3. A conventional close button. Closing is a single call.
  4. 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:

10: golden_apple, 12: player_head, 14: lava_bucket, 16: clock, 22: barrier

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:

PlayerListMenu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Six rows is the biggest menu.
  2. The top five rows, 45 slots, hold the players. The bottom row is for navigation buttons.
  3. The page the viewer is on, counted from 0. A normal field, so the menu remembers it between clicks.
PlayerListMenu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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    }
  1. Copies the online players into a list we can sort.
  2. Sorts by name, ignoring upper and lower case, so the order is stable between redraws.
  3. 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.
  4. Keeps the page number in range, even if players left while the menu was open.
  5. Takes just the players for this page, for example players 45 to 89 for the second page.
  6. Draws the player's own skin as the button icon, using a player head item.
  7. Admins get an extra lore line telling them the button teleports.
PlayerListMenu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The first slot of the last row. All the navigation buttons live there.
  2. Back button: opens the main menu again, which makes a menu a small tree of screens.
  3. Only show Previous when there is a previous page.
  4. Goes to the previous page...
  5. ...and redraws, so the window now shows the new page.
  6. An icon, not a button: it shows "Page 1 of 3" and does nothing when clicked.
  7. 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):

0: player_head, 1: player_head, 2: player_head, 3: player_head, 4: player_head, 5: player_head, 6: player_head, 7: player_head, 8: player_head, 9: player_head, 10: player_head, 11: player_head, 12: player_head, 13: player_head, 14: player_head, 15: player_head, 16: player_head, 17: player_head, 18: player_head, 19: player_head, 20: player_head, 21: player_head, 22: player_head, 23: player_head, 24: player_head, 25: player_head, 26: player_head, 27: player_head, 28: player_head, 29: player_head, 30: player_head, 31: player_head, 32: player_head, 33: player_head, 34: player_head, 35: player_head, 36: player_head, 37: player_head, 38: player_head, 39: player_head, 40: player_head, 41: player_head, 42: player_head, 43: player_head, 44: player_head, 45: oak_door, 48: arrow, 49: paper, 50: arrow, 53: sunflower

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:

ConfirmMenu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. A Runnable is a piece of code with no inputs and no result that you can run later by calling run(). It holds "what to do on yes".
  2. The question, such as "Clear your inventory?", becomes the window title.
  3. Green means yes, red means no. The colors tell players what to expect without reading.
  4. 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:

MainMenu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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));
  1. Opens a confirm screen when the lava bucket is clicked.
  2. The "yes" code: clear the inventory, tell the player and close the window.
  3. The dangerous action itself. It only runs after Confirm was clicked.
  4. The "no" code: simply go back to the main menu.
11: lime_concrete, 15: red_concrete

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
        • resources/
          • plugin.ymlName, main class and the guimenus.menu and guimenus.admin permissions
GuiMenusPlugin.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. One listener for all menus. Add new menus as classes, no new listener needed.
  2. The command lives inside the plugin class because it is tiny. A nested class is a class written inside another one.
  3. The whole command: create the menu, open it.
  4. Who may type /menu.
plugin.ymlCompiles on Paper 26.3Compile Lab
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
    • 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

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
  1. 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:

MenuTypeWay.javaCompiles on Paper 26.3Compile Lab
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

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}
  1. A chest window with 9 columns and 3 rows. There are also GENERIC_9X1 to GENERIC_9X6, plus hoppers, anvils, furnaces and more.
  2. Creates the view for that player with a title. It does not show it yet.
  3. The inventory you fill with buttons.
  4. Shows the window.
MenuTypeWay.javaCompiles on Paper 26.3Compile Lab
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

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}
  1. The classic way. Passing null as 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. Check getClickedInventory() first, or use getRawSlot() 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.
Try it

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.

MainMenu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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});
  1. Slot 11, between Heal and Online Players.
  2. The one-line action.
Try it

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.

MenuCenter.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The middle row, counted from 0. Division drops the remainder, so 4 / 2 is 2 and 5 / 2 would also be 2.
  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 with player.openInventory(...).
  • Slots are numbered from 0, left to right, top to bottom: slot = row * 9 + column.
  • An InventoryHolder class is the safe way to recognize your own menus. Never rely on the title.
  • Cancel every InventoryClickEvent in a menu window, in both inventories, and cancel InventoryDragEvent too. Handle button actions only for clicks in the top inventory.
  • A Menu base class with a map from slot to action keeps each new menu short. Filler panes tidy the gaps, and redraw updates the open window.
  • Paged menus compute pages with a rounded-up division, subList and Previous and Next buttons. A confirm menu takes the "yes" and "no" code from its caller.

Quick quiz

  1. Which slot is in row 2, column 4 of a chest menu (both counted from 0)?

  2. Why should the click listener cancel clicks in the player's own (bottom) inventory too, while a menu is open?

  3. How does the example recognize that an inventory belongs to its menu?

  4. A paged menu shows 45 players per page and 46 players are online. What does (46 + 45 - 1) / 45 give, and why use it?

  5. What is a Runnable used for in the confirm menu?

Next steps