Wiki 9Minecraft Wiki 9Minecraft

Modifying Server Forms in JSON UI

Customize and build multiple-layout menus triggered from the Script API using server_form.json.

5Sections
3Code examples
3Known errors

Server forms are the pop-up dialogs triggered by the `@minecraft/server-ui` Script API module, covering `ActionFormData`, `ModalFormData`, and `MessageFormData`. By modifying `RP/ui/server_form.json` in a resource pack, you can redesign these menus into custom shops, quest logs, or grid inventories without breaking standard forms used by other packs. Because all forms pass their title and button properties directly to the UI engine as data bindings, you can use conditional visibility to swap entire layouts based on a hidden string in the form's title.

How Bedrock Routes Server Forms

Every form shown from a script or dedicated server is handled by the `server_form` UI namespace defined in `RP/ui/server_form.json`. The root screen element is `third_party_server_screen`, which displays a factory bound to `#form_type`. Depending on which class you instantiate in your script, the game selects one of three templates inside that factory.

Understanding which script class maps to which JSON control is the first step to modifying them. If you only want to restyle button lists, you only need to touch the `long_form` template and its children.

Script API ClassUI Control NameDefault Layout
ActionFormDatalong_formScrolling vertical stack of body text and optional-icon buttons
ModalFormDatacustom_formScrolling list of toggles, sliders, dropdowns, and text boxes with a submit button
MessageFormDatapopup_formSimple two-button dialog box with body text

Key Data Bindings

Server forms communicate with JSON UI through hardcoded engine bindings. You cannot invent new bindings in script and pass them across directly; instead, you pack data into the existing strings (title, body, button text) and read them on the UI side.

Inside the `long_form` control tree, bindings are divided between screen-level properties (like the form title) and collection-level properties that repeat for each button inside the `form_buttons` collection.

Binding NameScopeWhat it holds
#title_textScreen / FormThe string passed to .title() in Script API
#form_textAction / Message FormThe string passed to .body() in Script API
#form_button_lengthlong_formTotal number of buttons in the form_buttons collection
#form_button_textform_buttons collectionThe label of the individual button
#form_button_textureform_buttons collectionTexture path or item icon path passed to .button()
#form_button_texture_file_systemform_buttons collectionIndicates whether the icon comes from the resource pack or raw file system

Routing Custom Layouts via Title Flags

If you overwrite `long_form` directly, every `ActionFormData` in the world will use your new layout. To keep vanilla forms intact while adding your own custom menus, modify the `long_form` control inside `third_party_server_screen` to contain both the vanilla panel and your custom panel, then toggle their `visible` properties by checking `#title_text`.

In JSON UI, subtracting a substring from `#title_text` and comparing it to the original `#title_text` is the standard way to check if a string contains a flag. If `(not ((#title_text - 'my_grid_flag') = #title_text))` evaluates to true, the flag is present. You should also strip the flag from the visible title label or give your custom window its own header.

RP/ui/server_form.json
{
  "namespace": "server_form",

  "third_party_server_screen/root_panel": {
    "control_ids": {
      "long_form": "@server_form.routed_long_form",
      "custom_form": "@server_form.custom_form"
    }
  },

  "routed_long_form": {
    "type": "panel",
    "size": ["100%", "100%"],
    "controls": [
      {
        "vanilla_fallback@server_form.long_form": {
          "bindings": [
            {
              "binding_name": "#title_text"
            },
            {
              "binding_type": "view",
              "source_property_name": "((#title_text - '§c§g§r') = #title_text)",
              "target_property_name": "#visible"
            }
          ]
        }
      },
      {
        "custom_grid_menu@server_form.my_custom_grid_form": {
          "bindings": [
            {
              "binding_name": "#title_text"
            },
            {
              "binding_type": "view",
              "source_property_name": "(not ((#title_text - '§c§g§r') = #title_text))",
              "target_property_name": "#visible"
            }
          ]
        }
      }
    ]
  }
}

Building a Custom Button Grid

Once you have routed a flagged form to your own control, you can replace the vertical stack with a `grid` or `stack_panel` of any shape. The critical requirement is that your repeating button template still uses `collection_name` set to `form_buttons` and maps the button press to `button.form_button_click`.

Below is a complete template for `my_custom_grid_form` that turns the button list into a 3-column grid using standard common UI dialog backgrounds and button templates. Each button displays its assigned texture icon and submits its index back to the Script API when clicked.

RP/ui/server_form.json
{
  "namespace": "server_form",

  "my_custom_grid_form@common_dialogs.main_panel_no_buttons": {
    "$title_panel": "common_dialogs.standard_title_label",
    "$title_size": ["100% - 14px", 10],
    "size": [220, 180],
    "$child_control": "server_form.grid_scrolling_panel"
  },

  "[email protected]_panel": {
    "size": ["100%", "100%"],
    "$scrolling_content": "server_form.button_grid",
    "$show_background": false
  },

  "button_grid": {
    "type": "grid",
    "size": ["100%", "default"],
    "grid_item_template": "server_form.grid_button_item",
    "grid_dimension_binding": "#form_button_length",
    "maximum_grid_items": 27,
    "collection_name": "form_buttons",
    "bindings": [
      {
        "binding_name": "#form_button_length"
      }
    ]
  },

  "grid_button_item": {
    "type": "panel",
    "size": ["33.33%", 40],
    "controls": [
      {
        "btn@common_buttons.light_content_button": {
          "size": ["100% - 4px", "100% - 4px"],
          "$pressed_button_name": "button.form_button_click",
          "$button_content": "server_form.grid_button_label",
          "bindings": [
            {
              "binding_type": "collection_details",
              "collection_name": "form_buttons"
            }
          ]
        }
      }
    ]
  },

  "grid_button_label": {
    "type": "label",
    "text": "#form_button_text",
    "bindings": [
      {
        "binding_name": "#form_button_text",
        "binding_type": "collection",
        "collection_name": "form_buttons"
      }
    ]
  }
}

Triggering the Custom Form from Script

In your behavior pack script, you do not need any special code to interact with the modified UI other than including your flag string in the `.title()` call. The button indices returned in `ActionFormResponse.selection` still correspond to the exact order in which you called `.button()`.

Using section-sign formatting codes (such as `§c§g§r`) as your flag is popular because if a player joins without the resource pack applied, the formatting codes are invisible in the default title bar.

BP/scripts/main.js
import { ActionFormData } from "@minecraft/server-ui";

export function showCustomGrid(player) {
  const form = new ActionFormData()
    .title("§c§g§rShop Menu")
    .body("Select an item to purchase:")
    .button("Apple", "textures/items/apple")
    .button("Bread", "textures/items/bread")
    .button("Diamond", "textures/items/diamond");

  form.show(player).then((response) => {
    if (response.canceled) return;
    player.sendMessage(`You clicked slot ${response.selection}`);
  });
}

Which versions this applies to

Applies to Bedrock Edition 1.19.0 and newer. JSON UI modifications in resource packs do not require any experimental toggles, and `@minecraft/server-ui` is stable in modern versions when using its release version in `manifest.json`.

When it does not work

What you seeWhyThe fix
Clicking a custom button plays the click sound, but the form does not close and the script never receives a response.The button control is missing the `collection_details` binding for `form_buttons`, or `$pressed_button_name` is not set to `button.form_button_click`.Ensure the interactive button control binds `"binding_type": "collection_details"` with `"collection_name": "form_buttons"` so the engine knows which button index was pressed.
Both the vanilla form and the custom form appear stacked on top of each other, or the screen is completely blank when the form opens.A syntax error in the `source_property_name` expression or a missing binding for `#title_text` on the same control that evaluates the visibility expression.Always declare `{ "binding_name": "#title_text" }` inside the `bindings` array right above the `view` binding that reads `#title_text`. Check that all parentheses in `(not ((#title_text - 'flag') = #title_text))` are balanced.
Content Log shows error: "Can't find control ... in namespace server_form" when a form is triggered.Instead of merging with the vanilla `RP/ui/server_form.json`, you replaced a required root element or misspelled a control reference inside `control_ids`.Only override the specific child keys you need (such as `third_party_server_screen/root_panel`) rather than redefining `third_party_server_screen` from scratch.

Reference, straight from the game files

Read out of the game itself, so the ids match the version you are building for.