DUXPLIMA Documentation

Custom Menu Engine

Writing a menu file: layout, items, sources, pagination and input.

A custom menu is a single HOCON file in plugins/uxmEssentials/menus/. The file name is the menu's name: menus/shop.conf opens with /menu open shop.

The same engine that draws every built-in menu loads your files, so everything on this page applies equally to both. A menu that fails to parse is skipped with a warning in the log: one bad file never hides the rest.

The smallest menu

title = "<dark_aqua>Hello Menu"
rows = 3

items {
  greet {
    slot = 13
    material = OAK_SIGN
    name = "<yellow>Say hello"
    lore = ["<gray>Click me."]
    click {
      left = ["message:<gold>Hello from a custom menu!"]
    }
  }
}

Save it as menus/hello.conf, run /menu reload, then /menu open hello. That is the whole loop.

Titles and text are verbatim MiniMessage, not catalog keys

Names, lore and titles in a custom menu are written as MiniMessage and rendered exactly as you type them. They are not run through the message catalogs, so style them however you like: "<gradient:#55ffff:#5555ff>Shop</gradient>". This is the one place in the plugin where inline text is expected rather than a @catalog.key.

These sit at the top of the file, outside items { }.

KeyMeaning
titleThe window title (MiniMessage; may hold %placeholders%).
rowsChest height, 1 to 6.
inventory-typeOptional non-chest shape (hopper, dispenser, …). Falls back to a rows chest if the shape rejects the window.
items { }The item map: each key is an item id.
open-requirementConditions that must pass before the menu opens at all (see below).
open-actions / close-actionsAction lists run when the menu opens / closes.
refresh { enabled, interval-ticks }Auto re-render on a timer. update-interval is the shorthand.
placeholders { }Menu-scoped custom placeholders: name = template. A menu-scoped token overrides the global registry for this menu only.
click-cooldownAnti-spam window in milliseconds; two clicks closer than this count as one.
layout / patterns / fill-itemGrid-string layout, reusable slot patterns, and the filler placed in every slot nothing else drew.
bottom-inventoryExtends the canvas into the player's own 36 slots (raw slots rows*9 …). Chest-only by construction.
chest-onlyKeep this menu on the chest path even for Bedrock viewers (see Bedrock Forms).
bedrock { }An explicit native Bedrock form for this menu.
command { }Give the menu its own open-command (see below).

Item keys

Each entry under items { } is one tile, keyed by an id you choose.

KeyMeaning
materialThe icon. A literal (DIAMOND_SWORD), a %placeholder%, or a prefixed icon spec (skull:, basehead:, hdb:, itemsadder:, oraxen:, nexo:, craftengine:, mmoitems:).
slot / slotsWhere it sits: a single slot, a list, or ranges.
priorityTie-break when two items claim the same slot; higher wins.
nameDisplay name (MiniMessage).
loreLore lines (a list of MiniMessage strings).
lore-modeHow this lore combines with the base icon's own lore (REPLACE is the default).
amountStack size shown on the icon.
decor { }Rich extras: model-data, glow, item flags, enchantments, potion/banner/trim, damage, data-components, and the tooltip keys below.
viewVisibility gate: hide the tile unless conditions pass (see below).
updatetrue to re-render this tile on every refresh tick.
typePagination role: NONE, NEXT, PREVIOUS, JUMP. See Page arrows.
list { }Expand one template tile across a data source (online players, worlds, …). See below.
click { }What each click gesture does (see below).

Arithmetic in a line

A name or lore line can carry {math: <expression>} anywhere in it, and the block is replaced by the number it evaluates to. Placeholders resolve first, so the expression sees values rather than tokens:

lore = [
  "<gray>Cost: <white>{math: %price% * %amount%}",
  "<gray>Left: <white>{math: max(0, %limit% - %used%)}"
]

The expression language is closed. Operators are + - * / % ^, with ^ right associative and a leading + or - allowed on a term. The functions are abs, floor, ceil, round, sqrt, and the variadic min and max; any other name is refused. There is no way to reach the server, the filesystem or a plugin from inside a block. A result that is a whole number prints without a trailing .0.

A block that cannot be evaluated renders as nothing, leaving a visible gap rather than the raw expression. That includes a block whose expression names a placeholder that resolves to nothing: {math: %missing% + 1} shows nothing, not 1. The one case still evaluated with a hole in it is a block a placeholder's own value supplies rather than one written in the file.

What the client is allowed to say

A menu icon is a button, and the client does not know that. Left alone it writes its own lines under the lore you wrote: the mining speed under an IRON_PICKAXE, Dyed under a tinted chestplate, the flight duration under a firework, in a colour the menu never chose.

The engine silences those lines by default, keeping the ones your own decor block asked for. If you wrote enchantments, trim, potion, leather-color, banner, unbreakable, attribute-modifiers or tool, that line is content and it stays; a shop tile selling a Sharpness V book still shows Sharpness V. Only the lines that come from the material alone go away.

Two keys inside decor { } change that:

decor {
  hide-vanilla-tooltip = false                      # give every line back
  hidden-components = ["dyed_color", "equippable"]  # or name an exact set
}

hide-vanilla-tooltip = false lets the client draw everything, which is what you want for a tile that shows a real item and nothing else. hidden-components names exactly what to silence and overrides the default; an id it cannot resolve is skipped rather than failing the render. The component ids are the vanilla ones (dyed_color, equippable, trim, fireworks, tool, attribute_modifiers, and so on).

This is about drawing, not about behaviour. Hiding equippable removes the line, not the equip: a menu stops the equip by cancelling the click, which it already does.

Two related keys are not the same thing. hide-tooltip = true removes the whole tooltip, your name and lore included, which is almost never what a menu wants. The item flags (flags = ["HIDE_ATTRIBUTES"]) still work, but they predate the components the client has gained since and cannot cover most of them; HIDE_ADDITIONAL_TOOLTIP in particular is deprecated on Paper 26.2. New menus should use the keys above instead. Flag names are case-insensitive and a hyphen reads as an underscore, so hide_attributes and hide-attributes name the same flag; a name that matches no flag is skipped.

List-backed items

A list { } block turns one tile into a whole grid: the engine asks a source for entries and stamps the template item once per entry across the item's slots.

players {
  slots = ["0-44"]
  list {
    source = "online-players"
    template {
      material = "skull:%entry_name%"
      name     = "<white>%entry_name%"
      click { left = ["command:msg %entry_name%"] }
    }
  }
}
KeyMeaning
sourceThe data source id.
template { }A normal item spec, stamped once per entry.
page-sizeEntries per page. Omit it (or 0) to derive the page from the item's slot count. Paged sources only.
sortsThe orderings the list-sort button cycles through. Paged sources only.
empty { }A normal item spec, drawn in each cell a page leaves without an entry. Omit it and those cells show the fill-item.
alignstart (the default) fills from the first slot. center puts the short last row of a page in the middle of its row.

%position% in the template or in the empty item is the place of that cell in the whole list, counted from one, so a leaderboard can number its rows and its empty places alike:

board {
  slots = ["10-16"]
  list {
    source = "kills:top"
    align = center
    template { material = "skull:%entry_name%", name = "<value>#%position% <body>%entry_name%" }
    empty    { material = SKELETON_SKULL, name = "<muted>#%position% nobody yet" }
  }
}

Page arrows

An item with type = next, previous or jump is drawn on every page. When there is no page to go to, pressing it does nothing. The file decides everything else:

You wantWrite
The arrow on every page (the default)nothing
The arrow only when it turns a pageview = ["has-next"], ["has-previous"] or, on a jump, ["has-page:3"]
Another look when there is no pagea second item in the same slot with view = ["!has-next"] and a lower priority
next    { slot = 23, type = next, material = ARROW, name = "@menu.button.next", priority = 10, view = ["has-next"] }
no-next { slot = 23, material = GRAY_DYE, name = "@menu.button.no-next", priority = 9, view = ["!has-next"] }

The fill

fill-item is painted into every slot nothing else drew when the window is drawn: a list cell with no entry and no empty item, and the slot of an item its view hid. A content region the player puts items into is never painted. To keep one slot empty on purpose, put an item there with material = AIR.

Built-in sources include online-players, worlds, self-inventory, self-enderchest and luckperms-groups; every module contributes its own (kits:browse, warps:browse, homes:invited-players, moderation:history, economy:transactions, worlds:list, …). The Menu API registers more.

Plain sources vs paged sources

There are two kinds, and the difference matters once a list gets long.

A plain source hands its whole collection over and the engine slices it into pages in memory. That is fine for the online players or the loaded worlds.

A paged source is asked for one page at a time, off the tick thread, with the viewer's current page, sort and filters pushed down to whatever holds the data. Only that page comes back, so a browse over ten thousand rows costs the same as a browse over ten. playerwarps:browse (behind /pwarp) is the reference example.

page-size and sorts are paged-only. Set either on a plain in-memory source and the spec is rejected at load with a message naming the item. That is deliberate: a knob that silently does nothing is worse than a loud error.

Sorting, filtering and searching

A paged list is steered from ordinary buttons, each naming the source id it targets (a menu may hold more than one list):

ActionDoes
list-sort:<source>Advance to the next sort in sorts. Add :prev or :reset for the other directions.
list-filter:<source>:<key>=<value>Set a filter. An empty value clears it.
list-search:<source>:<key>Open a text prompt and store the typed line as that filter.

The engine does not interpret sort or filter values, only the source does, so the vocabulary is whatever that source documents. From the shipped player-warp browse:

sort   { click { left = ["list-sort:playerwarps:browse"] } }
search { click { left = ["list-search:playerwarps:browse:search"] } }
mine   { click { left = ["list-filter:playerwarps:browse:scope=mine"] } }
all    { click { left = ["list-filter:playerwarps:browse:scope="] } }

State is per viewer and per open menu, so two players browsing the same menu sort and filter independently.

Asking the player to type something

Two click steps pause the action chain, ask the player for something, and resume with the answer:

  • input:<key> opens a text prompt; the typed line continues the chain.
  • confirm:<key> asks for a yes/no before the rest of the chain runs.

Both are top-level click steps. Written inside an else, a deny, or a per-requirement list they cannot resume anything, so they are ignored with a one-line warning in the console rather than failing silently.

How the prompt actually appears is operator config, not menu config. text-input.conf sets a global default-mode and per-point overrides:

ModeThe player sees
anvilA small anvil window; type in the rename field and click the result
chatThe menu closes and the next chat message is the answer
signA transient sign opens for editing
dialogA native dialog screen (Minecraft 1.21.6+)

Every prompt works in every mode, so switching one is always safe. dialog on an older server falls back to sign (or anvil), logged once so the substitution is not silent. Bedrock viewers always get a native form regardless of the mode.

Click blocks

click { } binds behaviour to gestures. Each gesture is either a bare action list or a block with actions, requirements, minimum, deny and an else fallback.

click {
  left  = ["command:spawn", "close"]
  right = ["open:warps"]
}

The gesture keys are left, right, shift-left, shift-right, middle, drop, control-drop, double-click, and any (a catch-all that fires alongside whichever gesture was used). The underscore forms (shift_left, double_click) are accepted too.

The full grammar of actions, conditions, per-gesture requirements and else ladders lives on the Actions & Requirements page.

A complete worked example

A three-row VIP shop tile: it only shows to players with a permission, charges money on click, and gives a diamond block, with a fallback message when they cannot afford it.

# menus/shop.conf   to   /menu open shop   (or /shop, see the command block)

title = "<gradient:#ffd700:#ff8c00>VIP Shop</gradient>"
rows = 3

# Gate the whole menu: only VIPs may open it at all.
open-requirement = ["perm:uxmessentials.vip"]

# A tidy grey border around the edges.
fill-item {
  material = GRAY_STAINED_GLASS_PANE
  decor { hide-tooltip = true }
}

items {
  buy-block {
    slots = [13]
    material = DIAMOND_BLOCK
    name = "<aqua>Diamond Block"
    lore = [
      "<gray>Price: <white>500</white>",
      "",
      "<yellow>Left-click to buy."
    ]
    decor { glow = true }

    # Hide this tile entirely from anyone in the 'spawn' world.
    view {
      requirements = ["!world:spawn"]
      minimum = 1
    }

    click {
      left {
        requirements = ["has-money:500"]
        actions = [
          "take-money:500",
          "give-item:DIAMOND_BLOCK",
          "sound:BLOCK_NOTE_BLOCK_PLING",
          "message:<green>Purchased a diamond block!"
        ]
        # Runs instead of 'actions' when the requirement fails.
        deny = ["message:<red>You need 500 to buy that."]
      }
    }
  }

  close {
    slot = 22
    material = BARRIER
    name = "<red>Close"
    click { left = ["close"] }
  }
}

Bare id:value, never brackets. Every action, condition and placeholder is a bare id:value reference: message:hi, perm:vip, has-money:500, open:shop. uxmEssentials does not use the bracketed [message] hi style some other menu plugins use. If you are converting an old file, this is the single most common thing to fix (the converters do it for you).

Giving a menu its own command

Add a top-level command { } block and the menu registers its own open-command. Now /shop opens menus/shop.conf directly: no /menu open needed.

command {
  name = "shop"                       # registers /shop
  aliases = ["store", "vipshop"]      # extra literals
  permission = "uxmessentials.shop.open"
  deny-message = "<red>The shop is closed."
  console = false                     # players only
  usage = "Open the VIP shop"
}

name must be a single lowercase word. A malformed or duplicate alias is dropped rather than aborting the command, and the menu still opens through /menu open shop even if the command block is invalid. You can also declare typed positional arguments here for commands like /gift <target> <amount>: the argument values become placeholders the menu can read.

The /menu command

/menu is the operator surface over the engine. open and list/last are for everyone (uxmessentials.menu.use); the rest are admin-gated with uxmessentials.menu.admin.

CommandWhat it doesPermission
/menu open <name> [target]Open a menu for yourself, or for another player.menu.use (target: .menu.open.others)
/menu listList every loaded menu.menu.use
/menu lastReopen the last menu you had open.menu.use
/menu reload [menu]Re-read the whole menus/ folder, or just one spec.menu.admin
/menu dump <menu>Print a menu's parsed structure (items, slots, clicks).menu.admin
/menu meta <menu>Print a menu's metadata summary.menu.admin

Reload after every edit. Menu files are read on load, on /menu reload and on /uxmess reload, never on a hot path, so nothing you type takes effect until you reload. /menu reload shop re-reads one file and tells you in the log exactly where a syntax error is; a bad file is skipped and the others keep working.

There are two more /menu subcommands covered elsewhere: /menu convert (see Converting Other Menus) and /menu execute (run a single menu action for a player, admin-only).

Related: Actions & Requirements, Bedrock Forms, Converting Other Menus, Menu API