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.
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) },
)