Skip to main content

Building Menus with GUI Extension

This guide walks you through creating menus of increasing complexity — from a basic 3-slot menu to a full multi-frame dashboard with persistent storage.

Table of Contents

  1. Your First Menu — Simple layout, 3 slots, interactions
  2. Grid Patterns — Using count, direction, gap, repeatY
  3. Scrollable Menus — Long lists with scroll buttons
  4. Multi-Frame Dashboards — Independent scrollable zones
  5. Paginated Collections — Multi-page browsing
  6. Persistent Storage — Saving items across sessions
  7. Vanilla GUIs — Anvil, Enchanting, Smithing
  8. Full Example: Shop Menu — Putting it all together

Your First Menu

A minimal menu with 3 clickable slots.

Architecture

Page JSON

{
  "type": "sequence",
  "name": "Simple Menu Demo",
  "entries": [
    {
      "id": "simple_menu",
      "name": "Simple Menu",
      "type": "open_gui",
      "guiType": "CUSTOM",
      "size": "SIZE_27",
      "title": "<gold>✦  <white>Simple Menu</white>  ✦",
      "mainLayoutId": "main",
      "layoutPool": [
        {
          "case": "simple",
          "value": {
            "id": "main",
            "items": [
              {
                "x": 2, "y": 1,
                "item": { "case": "minecraft_item", "value": { "material": "DIAMOND" } },
                "displayName": "<aqua>Diamond",
                "lore": ["<gray>Click to receive a diamond"],
                "interactionList": [
                  {
                    "type": "LEFT",
                    "commands": ["give %player% diamond 1"],
                    "closeMenu": true
                  }
                ]
              },
              {
                "x": 4, "y": 1,
                "item": { "case": "minecraft_item", "value": { "material": "EMERALD" } },
                "displayName": "<green>Emerald",
                "lore": ["<gray>Click to receive an emerald"],
                "interactionList": [
                  {
                    "type": "LEFT",
                    "commands": ["give %player% emerald 1"],
                    "closeMenu": true
                  }
                ]
              },
              {
                "x": 6, "y": 1,
                "item": { "case": "minecraft_item", "value": { "material": "GOLD_INGOT" } },
                "displayName": "<gold>Gold Ingot",
                "lore": ["<gray>Click to receive gold"],
                "interactionList": [
                  {
                    "type": "LEFT",
                    "commands": ["give %player% gold_ingot 1"],
                    "closeMenu": true
                  }
                ]
              }
            ]
          }
        }
      ]
    }
  ]
}

Key Concepts

  • x, y: Slot position on the 9x6 grid (0-indexed)
  • interactionList: Per-slot click actions. Each entry has a type (LEFT, RIGHT, etc.), commands, and optional closeMenu
  • %player%: Engine placeholder resolved to the player’s name
  • gui:close: Built-in command to close the menu (automatically added when closeMenu: true)

Grid Patterns

Use count, direction, gap, and repeatY to create grids without writing every position.

Architecture

Page JSON

{
  "x": 0, "y": 0,
  "item": { "case": "minecraft_item", "value": { "material": "GRAY_STAINED_GLASS_PANE" } },
  "displayName": " ",
  "count": 9,
  "direction": "right",
  "gap": 1,
  "repeatY": 3
}
This creates a full background of 27 gray glass panes.

Position Logic

FieldValueEffect
x, y0, 0Start at top-left
count99 items in the direction (fills width)
directionrightEach item is +1 in X
gap11 slot between items (standard)
repeatY3Repeat the row 3 times vertically

Example: Navigation Bar

{
  "x": 0, "y": 5,
  "count": 9,
  "direction": "right",
  "gap": 1,
  "repeatY": 1,
  "item": { ... }
}
Creates a 9-slot bar at the bottom of a SIZE_54 inventory.

Scrollable Menus

When content exceeds the visible area, use a ScrollableLayout.

Architecture

Page JSON

{
  "case": "scrollable",
  "value": {
    "id": "item_scroll",
    "virtualHeight": 12,
    "inner": {
      "case": "simple",
      "value": {
        "id": "item_list",
        "items": [
          { "x": 1, "y": 0, "item": { ... }, "displayName": "<aqua>Item 1" },
          { "x": 1, "y": 1, "item": { ... }, "displayName": "<aqua>Item 2" }
        ]
      }
    },
    "buttons": [
      {
        "x": 8, "y": 0,
        "direction": "UP",
        "step": 1,
        "item": { "case": "minecraft_item", "value": { "material": "PLAYER_HEAD" } },
        "displayName": "<green>▲ Scroll Up"
      },
      {
        "x": 8, "y": 5,
        "direction": "DOWN",
        "step": 1,
        "item": { "case": "minecraft_item", "value": { "material": "PLAYER_HEAD" } },
        "displayName": "<green>▼ Scroll Down"
      }
    ]
  }
}

Key Concepts

  • virtualHeight: Total scrollable height (in slots). The viewport shows 6 rows (SIZE_54) or 3 (SIZE_27).
  • buttons: Scroll buttons are anchored to the viewport — they don’t move with content
  • id: Required for each scrollable layout. Used internally for scroll tracking
  • Left/Right scroll: Set virtualWidth > 9 and add LEFT/RIGHT buttons

Multi-Frame Dashboards

Divide the screen into independent zones, each with its own scrolling content.

Architecture

Critical: Each scrollable zone must have a unique id. Without unique IDs, scroll events from zone B will incorrectly target zone A.

Page JSON

{
  "case": "frame",
  "value": {
    "id": "dashboard",
    "frames": [
      {
        "id": "zone_a",
        "x": 0, "y": 0,
        "width": 4, "height": 6,
        "layout": {
          "case": "scrollable",
          "value": {
            "id": "zone_a_scroll",
            "virtualHeight": 20,
            "inner": {
              "case": "simple",
              "value": {
                "items": [
                  { "x": 0, "y": 0, "item": { ... }, "displayName": "<aqua>Player 1" },
                  { "x": 0, "y": 1, "item": { ... }, "displayName": "<aqua>Player 2" }
                ]
              }
            },
            "buttons": [
              { "x": 3, "y": 0, "direction": "UP", "step": 1, "item": { ... } },
              { "x": 3, "y": 5, "direction": "DOWN", "step": 1, "item": { ... } }
            ]
          }
        }
      },
      {
        "id": "zone_b",
        "x": 5, "y": 0,
        "width": 4, "height": 6,
        "layout": {
          "case": "scrollable",
          "value": {
            "id": "zone_b_scroll",
            "virtualHeight": 20,
            "inner": {
              "case": "simple",
              "value": {
                "items": [
                  { "x": 0, "y": 0, "item": { ... }, "displayName": "<gold>Quest 1" },
                  { "x": 0, "y": 1, "item": { ... }, "displayName": "<gold>Quest 2" }
                ]
              }
            },
            "buttons": [
              { "x": 8, "y": 0, "direction": "UP", "step": 1, "item": { ... } },
              { "x": 8, "y": 5, "direction": "DOWN", "step": 1, "item": { ... } }
            ]
          }
        }
      }
    ]
  }
}

Frame Fields

FieldDescription
idFrame identifier (for referencing)
x, yFrame position in the inventory grid
width, heightFrame dimensions in slots
layoutChild layout (usually scrollable)

Paginated Collections

Multi-page browsing with next/previous buttons.

Architecture

Page JSON

{
  "case": "paginated",
  "value": {
    "id": "shop_pages",
    "pages": [
      {
        "case": "simple",
        "value": {
          "items": [
            { "x": 0, "y": 0, "item": { ... }, "displayName": "<aqua>Product 1" },
            { "x": 1, "y": 0, "item": { ... }, "displayName": "<aqua>Product 2" }
          ]
        }
      },
      {
        "case": "simple",
        "value": {
          "items": [
            { "x": 0, "y": 0, "item": { ... }, "displayName": "<gold>Product 21" },
            { "x": 1, "y": 0, "item": { ... }, "displayName": "<gold>Product 22" }
          ]
        }
      }
    ],
    "buttons": [
      {
        "x": 3, "y": 5, "direction": "LEFT",
        "item": { "case": "minecraft_item", "value": { "material": "ARROW" } },
        "displayName": "<green>← Previous"
      },
      {
        "x": 5, "y": 5, "direction": "RIGHT",
        "item": { "case": "minecraft_item", "value": { "material": "ARROW" } },
        "displayName": "<green>→ Next"
      }
    ]
  }
}

Persistent Storage

Save items in GUI slots that persist across sessions and server restarts.

Architecture

Required Setup

  1. Create the artifact entry:
{
  "id": "bank_storage",
  "name": "Bank Storage",
  "type": "gui_storage"
}
  1. Add storage to any layout slot:
{
  "x": 2, "y": 1,
  "item": { "case": "minecraft_item", "value": { "material": "CHEST" } },
  "displayName": "<green>✦ <white>{stored_name}</white> ✦",
  "lore": [
    "<gray>──────────────",
    "<aqua>Stored: <white>{stored_amount}/{stored_max}</white>",
    "",
    "<gray><i>Left-click: Place one",
    "<gray><i>Right-click: Take one",
    "<gray><i>Sneak+Left: Place all",
    "<gray><i>Double-click: Fill from inv"
  ],
  "storage": {
    "entry": "bank_storage",
    "maxAmount": 64,
    "forceStorage": true
  }
}

Storage Placeholders

PlaceholderResolves to
{stored_name}Item display name (empty if slot is empty)
{stored_amount}Current count (0 if empty)
{stored_max}Max capacity (from maxAmount, if unlimited)

Group-Based Storage

Share items between players (team bank, island chest):
"storage": {
  "entry": "team_bank",
  "group": "island_group_entry",
  "maxAmount": 64
}

Accumulation Mode

Track progress towards a goal:
"storage": {
  "entry": "quest_deposit",
  "requiredItem": { "case": "minecraft_item", "value": { "material": "DIAMOND" } },
  "requiredAmount": 64,
  "onReachRequired": ["quest_complete"],
  "consumeOnReach": true
}
When 64 diamonds are deposited, onReachRequired triggers fire and items are consumed.
[!WARNING] Do not leave requiredItem as an empty object. In Typewriter’s editor, if you add a requiredItem field but leave it empty (no components), it resolves to Material.AIR at runtime. The slot will reject all items silently, because no item matches AIR. Either omit requiredItem entirely (for standard storage with no item restriction) or set it to a specific item type.

Click Configuration

Default click mappings (configurable globally via gui_settings):
ActionDefault Click
Place oneLEFT
Place allSHIFT_LEFT
Take oneRIGHT
Take allSHIFT_RIGHT
Take stackSWAP_OFFHAND
Fill from invDOUBLE_CLICK
Drop allDROP

Vanilla GUIs

Use Minecraft’s built-in GUI types for native behavior.

Available Types

guiTypeUse Case
ANVILRename/repair items
ENCHANTING_TABLEEnchant items
SMITHINGUpgrade gear to Netherite
STONECUTTERPrecise block cutting
GRINDSTONERepair/disenchant
LOOMBanner patterns
CARTOGRAPHYMap editing
MERCHANTVillager trades
BOOKDisplay text

Example: Anvil Rename

{
  "type": "open_gui",
  "guiType": "ANVIL",
  "title": "<gray>Rename Item",
  "mainLayoutId": "anvil_main",
  "layoutPool": [
    {
      "case": "simple",
      "value": {
        "id": "anvil_main",
        "items": [
          {
            "x": 0, "y": 0,
            "item": { "case": "minecraft_item", "value": { "material": "DIAMOND_SWORD" } },
            "displayName": "<aqua>Diamond Sword",
            "lore": ["<gray>Rename me!"]
          }
        ]
      }
    }
  ]
}
Note: Vanilla GUIs have fixed slot positions. Layout items are informational only.

Full Example: Shop Menu

A complete shop menu combining SimpleLayout, Storage, interactions, and navigation.

Architecture

Full JSON

{
  "id": "shop_demo",
  "name": "Shop Demo Menu",
  "type": "open_gui",
  "guiType": "CUSTOM",
  "size": "SIZE_27",
  "title": "<dark_aqua>✦  <white>Shop Demo</white>  ✦",
  "mainLayoutId": "shop_main",
  "layoutPool": [
    {
      "case": "simple",
      "value": {
        "id": "shop_main",
        "items": [
          {
            "x": 0, "y": 0,
            "count": 9,
            "direction": "right",
            "gap": 1,
            "repeatY": 3,
            "item": { "case": "minecraft_item", "value": { "material": "GRAY_STAINED_GLASS_PANE" } },
            "displayName": " "
          },
          {
            "x": 2, "y": 1,
            "item": { "case": "minecraft_item", "value": { "material": "CHEST" } },
            "displayName": "<green>✦ <white>{stored_name}</white> ✦",
            "lore": [
              "<gray>──────────────",
              "<aqua>Stored: <white>{stored_amount}/{stored_max}</white>",
              "",
              "<gray><i>Left: Place  |  Right: Take",
              "<gray><i>Sneak+Left: Fill  |  Double: Fill inv"
            ],
            "storage": {
              "entry": "shop_storage",
              "maxAmount": 10,
              "forceStorage": true
            }
          },
          {
            "x": 4, "y": 1,
            "item": { "case": "minecraft_item", "value": { "material": "CHEST" } },
            "displayName": "<gold>✦ <white>{stored_name}</white> ✦",
            "lore": [
              "<gray>──────────────",
              "<yellow>Stored: <white>{stored_amount}/{stored_max}</white>"
            ],
            "storage": {
              "entry": "shop_storage",
              "maxAmount": 64,
              "forceStorage": true
            }
          },
          {
            "x": 6, "y": 1,
            "item": { "case": "minecraft_item", "value": { "material": "CHEST" } },
            "displayName": "<red>✦ <white>{stored_name}</white> ✦",
            "lore": [
              "<gray>──────────────",
              "<red>Stored: <white>{stored_amount}/{stored_max}</white>"
            ],
            "storage": {
              "entry": "shop_storage",
              "maxAmount": 5,
              "forceStorage": true
            }
          },
          {
            "x": 2, "y": 2,
            "item": { "case": "minecraft_item", "value": { "material": "ENDER_CHEST" } },
            "displayName": "<light_purple>✦ <white>Single Item</white> ✦",
            "lore": ["<light_purple>Stores one item at a time"],
            "storage": {
              "entry": "shop_storage",
              "maxAmount": 1,
              "forceStorage": true
            }
          },
          {
            "x": 5, "y": 2,
            "item": { "case": "minecraft_item", "value": { "material": "BOOK" } },
            "displayName": "<gold>✦ <white>Shop Info</white> ✦",
            "lore": [
              "<gray>──────────────",
              "<green>4 storage slots",
              "",
              "<dark_aqua>Controls:",
              "<gray><i>Left: Place | Right: Take",
              "<gray><i>Sneak+Left: Place all",
              "<gray><i>Double-click: Fill from inv"
            ],
            "isGhost": true
          }
        ]
      }
    }
  ]
}

What This Example Demonstrates

FeatureUsed In
Grid backgroundGray glass panes with count=9, repeatY=3
Storage with placeholdersAll 4 chest slots use {stored_name}, {stored_amount}, {stored_max}
Different max amounts10, 64, 5, 1 — each slot has its own capacity
forceStorageAll storage slots accept any item type
isGhostInfo book can be clicked but not taken
Helpful loreEach slot shows click instructions

Best Practices

  1. Always use unique IDs for scrollable layouts in multi-frame menus
  2. Include click instructions in slot lore so users know what to do
  3. Use {stored_max} in lore — when max=0 or unlimited, it shows
  4. Set forceStorage: true for slots that may hold tools or non-stackable items
  5. Test with all inventory sizes — SIZE_27 (3 rows), SIZE_54 (6 rows)
  6. Use grid patterns (count/direction/repeatY) instead of duplicating items
  7. Group storage with a GroupEntry for team-based inventories
  8. onFill/onEmpty triggers for reactive menus that change when items are deposited