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 Class | UI Control Name | Default Layout |
|---|---|---|
| ActionFormData | long_form | Scrolling vertical stack of body text and optional-icon buttons |
| ModalFormData | custom_form | Scrolling list of toggles, sliders, dropdowns, and text boxes with a submit button |
| MessageFormData | popup_form | Simple 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 Name | Scope | What it holds |
|---|---|---|
| #title_text | Screen / Form | The string passed to .title() in Script API |
| #form_text | Action / Message Form | The string passed to .body() in Script API |
| #form_button_length | long_form | Total number of buttons in the form_buttons collection |
| #form_button_text | form_buttons collection | The label of the individual button |
| #form_button_texture | form_buttons collection | Texture path or item icon path passed to .button() |
| #form_button_texture_file_system | form_buttons collection | Indicates 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.
{
"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.
{
"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.
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 see | Why | The 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.
