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.
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.
Menu-level keys
These sit at the top of the file, outside items { }.
| Key | Meaning |
|---|---|
title | The window title (MiniMessage; may hold %placeholders%). |
rows | Chest height, 1 to 6. |
inventory-type | Optional 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-requirement | Conditions that must pass before the menu opens at all (see below). |
open-actions / close-actions | Action 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-cooldown | Anti-spam window in milliseconds; two clicks closer than this count as one. |
layout / patterns / fill-item | Grid-string layout, reusable slot patterns, and the filler placed in every slot nothing else drew. |
bottom-inventory | Extends the canvas into the player's own 36 slots (raw slots rows*9 …). Chest-only by construction. |
chest-only | Keep 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.
| Key | Meaning |
|---|---|
material | The icon. A literal (DIAMOND_SWORD), a %placeholder%, or a prefixed icon spec (skull:, basehead:, hdb:, itemsadder:, oraxen:, nexo:, craftengine:, mmoitems:). |
slot / slots | Where it sits: a single slot, a list, or ranges. |
priority | Tie-break when two items claim the same slot; higher wins. |
name | Display name (MiniMessage). |
lore | Lore lines (a list of MiniMessage strings). |
lore-mode | How this lore combines with the base icon's own lore (REPLACE is the default). |
amount | Stack 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. |
view | Visibility gate: hide the tile unless conditions pass (see below). |
update | true to re-render this tile on every refresh tick. |
type | Pagination 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%"] }
}
}
}
| Key | Meaning |
|---|---|
source | The data source id. |
template { } | A normal item spec, stamped once per entry. |
page-size | Entries per page. Omit it (or 0) to derive the page from the item's slot count. Paged sources only. |
sorts | The 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. |
align | start (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 want | Write |
|---|---|
| The arrow on every page (the default) | nothing |
| The arrow only when it turns a page | view = ["has-next"], ["has-previous"] or, on a jump, ["has-page:3"] |
| Another look when there is no page | a 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):
| Action | Does |
|---|---|
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:
| Mode | The player sees |
|---|---|
anvil | A small anvil window; type in the rename field and click the result |
chat | The menu closes and the next chat message is the answer |
sign | A transient sign opens for editing |
dialog | A 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.
| Command | What it does | Permission |
|---|---|---|
/menu open <name> [target] | Open a menu for yourself, or for another player | .menu.use (target: .menu.open.others) |
/menu list | List every loaded menu | .menu.use |
/menu last | Reopen 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
Was this page helpful?