Skip to content
BasaltSoftware
Bedrock 1.26.60

Site colour

Discord
Esc

    ↑↓ moveEnter openEsc close

    Keyboard shortcuts
    Ctrl K
    Search the whole site, from any page
    /
    Jump to the sidebar search, or search the whole site where there isn't one
    Enter
    Open the top search result
    Esc
    Clear the search, or close this panel
    ?
    Show these shortcuts

    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" }

    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 response

    show 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.

    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
    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.

    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.

    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.

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

    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.isValid before acting on the response.
    • Chain forms by showing the next one after the first resolves. Chained forms can simply await one another.
    • Retry when busy. If cancelationReason is FormCancelationReason.UserBusy, wait a few ticks with system.waitTicks and show the form again.
    • Forms are rate limited. A client that sends form answers faster than max_form_responses_per_second is 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.