Layout Types
Layouts are the building blocks of a menu. Each one is declared in the layoutPool as {"case": "<type>", "value": {...}} and referenced by id.
| Layout | Use it for | Nests children? |
|---|---|---|
simple | Static grids: borders, buttons, fixed content | — |
composite | Stacking several layouts (background + content) | ✔ by id |
scrollable | Content bigger than the window (lists, maps) | ✔ one inner |
paginated | Discrete pages with prev/next buttons | — |
frame | Independent zones (sidebar + main panel) | ✔ per frame |
flex | Auto-arranged rows (centered button bars) | — |
storage | Persistent item slots (deposits, crafting inputs) | — |
book / merchant | Vanilla book pages / trading UI | — |
Positions are virtual coordinates: x = column, y = row, starting at (0,0) top-left. The viewport shows 9 columns × the inventory's rows; anything outside is clipped (and reachable by scrolling, if wrapped in a scrollable).
simple — Static grid
A flat list of items at fixed positions. The workhorse layout.
{ "case": "simple", "value": {
"id": "border",
"items": [
{ "x": 0, "y": 0, "count": 9, "direction": "right",
"item": { "material": "GRAY_STAINED_GLASS_PANE" }, "displayName": " " }
]
}}
Repetition fields (avoid copy-pasting the same pane 9 times):
| Field | Meaning |
|---|---|
count | How many copies to place |
direction | right, left, down, up — required to repeat |
gap | Step between copies (default 1 = adjacent; 2 = every other slot) |
repeatY | Repeat the whole line N times on the other axis |
direction is required for repetition. With no direction, count, gap and repeatY are ignored and you get a single slot at (x, y). A row of 9 panes needs both "count": 9 and "direction": "right" — count on its own does nothing. This is the #1 reason a border "doesn't spread".
A full border needs four items — one per edge — because a straight repeat can't turn corners. For a 6-row menu:
| Edge | x | y | direction | count |
|---|---|---|---|---|
| Top | 0 | 0 | right | 9 |
| Bottom | 0 | 5 | right | 9 |
| Left | 0 | 1 | down | 4 |
| Right | 8 | 1 | down | 4 |
For an R-row menu: the bottom row is y = R-1, and the left/right edges use count = R-2 — they fill only the rows between top and bottom (the top/bottom full rows already cover the four corners).
composite — Stack layers
Combines several layouts by id. Later children render on top of earlier ones — order is your z-order.
{ "case": "composite", "value": {
"id": "main",
"children": ["background", "content", "nav_buttons"]
}}
Use for: the root of almost every menu (background + content + navigation).
scrollable — Viewport over large content
Wraps one inner layout (referenced by innerId) whose virtual size can exceed the window. The player scrolls the viewport across it.
{ "case": "scrollable", "value": {
"id": "list_scroll",
"innerId": "list_content",
"virtualWidth": 9,
"virtualHeight": 20,
"buttons": [
{ "direction": "UP", "step": 1,
"item": { "x": 8, "y": 0, "item": { "material": "ARROW" }, "displayName": "<white>▲" } },
{ "direction": "DOWN", "step": 1,
"item": { "x": 8, "y": 2, "item": { "material": "ARROW" }, "displayName": "<white>▼" } }
]
}}
virtualHeight is the total content height (20 rows of content → 20), not the visible height. Navigation buttons only appear when the content actually overflows.
Key points:
- Buttons are yours to define via
buttons(direction:UP/DOWN/LEFT/RIGHT,step= slots per click). There are no automatic default arrows. - Buttons are anchored to the viewport — they stay in place while content scrolls behind them.
- Scrolling can also come from interactions:
gui:scroll 0 1 list_scrollon any slot, or mouse-wheel/WASD bindings. - Each scrollable has its own scroll state in the session, tracked by its
id— you can have several independent scroll areas in one menu.
paginated — Discrete pages
Splits a flat item list into pages of itemsPerPage, laid out on the given slots indices, with optional prev/next buttons.
{ "case": "paginated", "value": {
"id": "shop_pages",
"itemsPerPage": 21,
"slots": [10,11,12,13,14,15,16, 19,20,21,22,23,24,25, 28,29,30,31,32,33,34],
"items": [ /* one entry per product; they flow page by page */ ],
"previousPage": { "item": { "x": 0, "y": 5, "item": { "material": "ARROW" }, "displayName": "<white>◀ Previous" } },
"nextPage": { "item": { "x": 8, "y": 5, "item": { "material": "ARROW" }, "displayName": "<white>Next ▶" } }
}}
slotsare physical indices (index = y*9 + x); items fill them in order.- Prev/next buttons appear only when that direction exists (no "previous" on page 1).
- Page state is per-player and per-layout-id.
Scrollable vs paginated? Scrolling feels continuous (maps, long lists); pagination is better for discrete catalogs (shops) and shows a stable grid per page.
frame — Independent zones
Divides the window into rectangles, each rendering its own layout with its own coordinate space. (0,0) inside a frame is the frame's top-left corner.
{ "case": "frame", "value": {
"id": "split_view",
"frames": [
{ "id": "sidebar", "x": 0, "y": 0, "width": 2, "height": 6, "layoutId": "sidebar_content" },
{ "id": "panel", "x": 3, "y": 0, "width": 6, "height": 6, "layoutId": "panel_content" }
]
}}
Use for: sidebar + main panel UIs, tab bars, dashboards. Combine with scrollable inside a frame for a scrolling sidebar next to static content.
flex — Auto-arranged rows
Positions items automatically with CSS-Flexbox-like rules — no manual coordinates.
{ "case": "flex", "value": {
"id": "action_bar",
"justifyContent": "CENTER",
"alignItems": "END",
"wrap": true,
"items": [ /* buttons — x/y are ignored, order matters */ ]
}}
justifyContent:START,CENTER,END,SPACE_BETWEEN— horizontal distribution.alignItems:START,CENTER,END— vertical placement of the row block.wrap: overflowing items continue on the next row.
Use for: button bars that stay centered regardless of how many buttons you add.
Addressable views
Views are a menu-level feature rather than a layout type. Use a frame layout as the shell and set a frame's layoutId to @view. Each MenuViewData then supplies the layout for that frame, either explicitly through frames or through the <viewId>_<frameId> naming convention. This keeps tabs, borders, and navigation shared while only the active content changes.
Slots tagged with buttonPrefix: "view:" and buttonType: "<viewId>" become view tabs. The GUI extension wires their click to gui:view <viewId>, hides tabs gated by criteria or permissions, and applies the configured active style. Parent views provide breadcrumbs and keep their tab active for nested screens.
See the complete Addressable Views guide for inheritance, history, and a full YAML example.
storage — Persistent item slots
Real slots the player can put items into; contents persist in a gui_storage artifact, per player or per group. See the gui_storage entry for click behaviors, accumulation mode and placeholders ({stored_name}, {stored_amount}, {stored_max}).
{ "case": "storage", "value": {
"id": "deposit",
"entry": "<gui_storage artifact id>",
"groupKey": "island_%island_id%",
"slots": [
{ "x": 3, "y": 1, "maxStack": 64,
"placeholder": { "material": "LIGHT_GRAY_STAINED_GLASS_PANE" } },
{ "x": 5, "y": 1, "temporary": true,
"requiredItem": { "material": "DIAMOND" }, "requiredAmount": 8,
"onReachRequired": ["<trigger entry id>"] }
]
}}
temporary: true— contents are returned/cleared when the menu closes (crafting inputs).requiredItem+requiredAmount+onReachRequired— fire triggers when a quota is met (quests, crafting).groupKeysupports placeholders → shared storages (island chests, team banks).
book — Vanilla book UI
A special top-level layout that opens a written book instead of a chest
window, with MiniMessage-formatted pages.
It cannot be nested inside another layout.
merchant — Vanilla trading UI
A special top-level layout that opens a villager trading screen with
configured trades (item costs and results). Input items are safely returned
when the menu closes.
It cannot be nested inside another layout.
API-only layouts & widgets
Extensions building menus in Kotlin (via MenuBuilder) also get:
IteratorLayout— auto-paginates any collection into a slot region.SliderComponent— a draggable value slider rendered as a track + thumb.ReactiveSlot— a slot whose item is resolved per-player at render time, with an optionalonClickcallback (live counters, custom buttons).
State-driven widgets built on ReactiveSlot (auto-repaint on click):
toggle(x, y, state, onItem, offItem, onToggle)— a boolean switch; the caller owns the persisted state, the widget reads and flips it.progressBar(x, y, width, progress, filled, empty, partial?)— a 0.0–1.0 bar spanningwidthslots with an optional boundary-cell item.tabs(x, y, tabs, active, onSelect)— a horizontal tab strip; eachTabSpecdefines its active/inactive item.
menu.toggle(
x = 4, y = 1,
state = stateOf { player -> settings.isPvpEnabled(player) },
onItem = pvpOnItem, offItem = pvpOffItem,
onToggle = { player, enabled -> settings.setPvp(player, enabled) },
)