Show button menus, confirmation dialogs and input forms with @minecraft/server-ui.
Forms are the pop-up screens a plugin can show a player: a list of buttons, a yes/no question, or a set of inputs. They come from the @minecraft/server-ui module and work as they do on BDS. Basalt adds one form type of its own, CustomForm.
Add the module to your manifest’s dependencies:
{ "module_name": "@minecraft/server-ui", "version": "2.0.0" }How forms work
Section titled “How forms work”Every form follows the same pattern: build it, call show(player), and await the response.
const response = await form.show(player);if (response.canceled) return;// use the responseshow returns a promise that resolves when the player answers or closes the form. Always check canceled first. When it is true, cancelationReason says why:
FormCancelationReason |
Meaning |
|---|---|
UserClosed |
The player closed the form. |
UserBusy |
The form could not open, for example because the player had chat or another screen open. |
The promise rejects with a FormRejectError only when the answer could not be read at all: the player left, Basalt is stopping, or the client sent a malformed response.
Action form: a list of buttons
Section titled “Action form: a list of buttons”import { world, Player } from "@minecraft/server";import { ActionFormData } from "@minecraft/server-ui";
const warps = [ { name: "Spawn", location: { x: 0, y: 80, z: 0 } }, { name: "Arena", location: { x: 200, y: 70, z: -40 } },];
async function openWarps(player: Player): Promise<void> { const form = new ActionFormData().title("Warps").body("Where to?"); for (const warp of warps) form.button(warp.name);
const response = await form.show(player); if (response.canceled || response.selection === undefined) return;
const warp = warps[response.selection]; if (player.isValid) player.teleport(warp.location);}
// Open the menu when a player uses a compass.world.afterEvents.itemUse.subscribe(({ source, itemStack }) => { if (itemStack.typeId === "minecraft:compass") void openWarps(source);});selection is the index of the button pressed, counting buttons only.
| Method | Adds |
|---|---|
title(text) |
The title bar |
body(text) |
Text above the buttons |
button(text, iconPath?) |
A button. The icon is a resource pack texture path or an http(s) address. |
header(text), label(text), divider() |
Headings, extra text and separators between buttons |
Message form: a question with two buttons
Section titled “Message form: a question with two buttons”import { Player } from "@minecraft/server";import { MessageFormData } from "@minecraft/server-ui";
async function confirmReset(player: Player): Promise<boolean> { const response = await new MessageFormData() .title("Reset island") .body("This deletes everything you have built. Are you sure?") .button1("Yes, reset") .button2("Cancel") .show(player);
return !response.canceled && response.selection === 0;}selection is 0 for button1 and 1 for button2.
Modal form: inputs
Section titled “Modal form: inputs”A modal form collects several values and has a submit button.
import { Player } from "@minecraft/server";import { ModalFormData } from "@minecraft/server-ui";
async function openSettings(player: Player): Promise<void> { const response = await new ModalFormData() .title("Settings") .textField("Nickname", "Type a nickname", { defaultValue: player.name }) .toggle("Show scoreboard", { defaultValue: true }) .slider("Chat range", 10, 100, { valueStep: 10, defaultValue: 50 }) .dropdown("Team", ["Red", "Blue", "Green"], { defaultValueIndex: 0 }) .submitButton("Save") .show(player);
if (response.canceled || !response.formValues) return;
const [nickname, showScoreboard, chatRange, team] = response.formValues; player.sendMessage(`Saved: ${nickname}, ${showScoreboard}, ${chatRange}, team #${team}`);}formValues has one entry per element, in the order they were added:
| Element | Value |
|---|---|
textField |
The text, a string |
toggle |
true or false |
slider |
The number chosen |
dropdown |
The index of the chosen item |
header, label, divider |
undefined. They still take a place in the list. |
Custom form (Basalt)
Section titled “Custom form (Basalt)”CustomForm is Basalt’s own. It uses the client’s data-driven screen, and differs from a modal form in one useful way: its buttons call back as they are pressed, while the screen stays open.
import { Player } from "@minecraft/server";import { CustomForm } from "@minecraft/server-ui";
async function openKitScreen(player: Player): Promise<void> { const form = new CustomForm("Kits") .label("Pick a kit. You can take more than one.") .button("Starter", () => player.sendMessage("Starter kit given")) .button("Miner", () => player.sendMessage("Miner kit given")) .toggle("Notify me about new kits", true) .closeButton();
const response = await form.show(player); const [, , , notify] = response.values; player.sendMessage(`Closed (${response.reason}); notifications: ${notify}`);}show resolves when the screen closes. values has one entry per element, and reason says why it closed.
| Method | Adds |
|---|---|
button(label, onClick, options?) |
A button whose callback runs when pressed |
textField(label, defaultValue?, options?) |
A text input |
toggle(label, defaultValue?, options?) |
A switch |
slider(label, min, max, defaultValue?, options?) |
A slider of whole numbers |
dropdown(label, items, defaultIndex?, options?) |
A dropdown |
label(text), header(text), spacer() |
Text, a heading and a gap |
closeButton(label?) |
The X in the corner |
CustomForm does not exist on BDS.
Closing forms from the plugin
Section titled “Closing forms from the plugin”uiManager closes what a player has open:
import { Player } from "@minecraft/server";import { uiManager } from "@minecraft/server-ui";
function closeEverything(player: Player): void { uiManager.closeAllForms(player); // action, message and modal forms uiManager.closeScreen(player); // a CustomForm (Basalt)}A page in the Settings screen (Basalt)
Section titled “A page in the Settings screen (Basalt)”uiManager.setServerSettings adds a modal form to the player’s own Settings screen. The callback receives their answer.
import { world } from "@minecraft/server";import { ModalFormData, uiManager } from "@minecraft/server-ui";
world.afterEvents.playerSpawn.subscribe(({ player, initialSpawn }) => { if (!initialSpawn) return; const page = new ModalFormData() .title("Server preferences") .toggle("Join messages", { defaultValue: true });
uiManager.setServerSettings(player, page, (response) => { if (response.canceled || !response.formValues) return; player.setDynamicProperty("prefs:joinMessages", response.formValues[0] === true); });});- Check the player is still there. A form can stay open for a long time. Test
player.isValidbefore acting on the response. - Chain forms by showing the next one after the first resolves. Chained forms can simply
awaitone another. - Retry when busy. If
cancelationReasonisFormCancelationReason.UserBusy, wait a few ticks withsystem.waitTicksand show the form again. - Forms are rate limited. A client that sends form answers faster than
max_form_responses_per_secondis kicked; normal use never comes close. - For an inventory-style menu with item icons, use Container menus instead.
The full list of classes is in @minecraft/server-ui.