~/dev.log/java-plugin-guis

The classic "custom GUI" in a Bukkit/Paper plugin is a chest inventory you open, fill with ItemStacks as buttons, and intercept clicks on. There's no special "GUI API" in vanilla Bukkit, it's the inventory system, repurposed.

Creating and opening a menu

import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;

public class KitMenu {
    public static Inventory create() {
        Inventory inv = Bukkit.createInventory(null, 27, "§8Choose a Kit");

        inv.setItem(11, namedItem(Material.IRON_SWORD, "§aWarrior"));
        inv.setItem(13, namedItem(Material.BOW, "§eArcher"));
        inv.setItem(15, namedItem(Material.DIAMOND_PICKAXE, "§bMiner"));

        return inv;
    }

    private static ItemStack namedItem(Material mat, String name) {
        ItemStack item = new ItemStack(mat);
        ItemMeta meta = item.getItemMeta();
        meta.setDisplayName(name);
        item.setItemMeta(meta);
        return item;
    }

    public static void open(Player player) {
        player.openInventory(create());
    }
}

Handling clicks

Always cancel the click event for a GUI, or players can pull items straight out of your menu:

import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.entity.Player;

public class KitMenuListener implements Listener {
    @EventHandler
    public void onClick(InventoryClickEvent event) {
        if (!"§8Choose a Kit".equals(event.getView().getTitle())) return;
        event.setCancelled(true);

        if (event.getClickedInventory() == null) return;
        Player player = (Player) event.getWhoClicked();

        switch (event.getSlot()) {
            case 11 -> giveKit(player, "warrior");
            case 13 -> giveKit(player, "archer");
            case 15 -> giveKit(player, "miner");
            default -> { return; }
        }
        player.closeInventory();
    }
}

Matching on the title string is the simplest approach but gets fragile with many menus. Once you have more than two or three screens, switch to a registry pattern (below).

A reusable menu framework

Most projects converge on the same shape: a Menu interface with getInventory() and onClick(slot, player), tracked in a map keyed by the open Inventory instance so the listener can dispatch generically instead of string-matching titles:

public interface Menu {
    Inventory getInventory();
    void onClick(int slot, Player player);
}

public class MenuManager implements Listener {
    private final Map<Inventory, Menu> openMenus = new HashMap<>();

    public void open(Player player, Menu menu) {
        openMenus.put(menu.getInventory(), menu);
        player.openInventory(menu.getInventory());
    }

    @EventHandler
    public void onClick(InventoryClickEvent event) {
        Menu menu = openMenus.get(event.getInventory());
        if (menu == null) return;
        event.setCancelled(true);
        menu.onClick(event.getSlot(), (Player) event.getWhoClicked());
    }

    @EventHandler
    public void onClose(InventoryCloseEvent event) {
        openMenus.remove(event.getInventory());
    }
}

Each concrete menu (e.g. KitMenu) implements Menu and builds its own ItemStacks, no more title-string matching, and menus can hold their own state (pagination index, selected filters, etc.) as instance fields.

Writing this by hand is a great learning exercise, but for real projects most devs pull in a small menu library rather than reinventing pagination/animation:

Add via Maven (shaded, since these aren't on the server already):

<dependency>
  <groupId>dev.triumphteam</groupId>
  <artifactId>triumph-gui</artifactId>
  <version>3.1.10</version>
</dependency>

Next: Economy, Permissions & Commands.