Wiki 9Minecraft Wiki 9Minecraft

JSON UI Best Practices

How to structure, modify, and maintain custom user interfaces without breaking vanilla screens.

6Sections
4Code examples
4Known errors

JSON UI is the internal system Minecraft Bedrock uses to layout and bind data to every screen in the game. Because this system was never formally exposed as a stable add-on API, modifying it requires strict defensive habits. Following these practices ensures your custom interfaces survive game updates and can run alongside other resource packs.

Modify with array operations, never full replacements

When you put a file such as hud_screen.json into your resource pack, the game merges your file on top of the vanilla one from the bottom of the pack stack upward. If you copy the entire vanilla file into your pack and edit a few lines in the middle, you overwrite every single control in that file.

This causes two immediate problems. First, the next time a game update adds a new button or changes a vanilla binding on that screen, your pack will erase the new vanilla element or crash the screen because a required binding is missing. Second, your pack will be completely incompatible with any other resource pack touching the same screen.

Instead, declare only the namespace and the specific control you need to alter, then use the modifications array. The engine supports operations like insert_back, insert_front, remove, and replace to surgically alter a control's children without redefining the parent.

OperationWhat it doesNotes
insert_backAppends your controls to the end of the target arraySafest option for adding overlays; renders after earlier children
insert_frontPrepends your controls to the start of the target arrayUseful when your element needs to sit behind existing children
insert_afterPlaces your controls immediately after a named childRequires control_name or where to identify the target sibling
insert_beforePlaces your controls immediately before a named childRequires control_name or where to identify the target sibling
replaceSwaps out a matched child control with your ownFails silently if another pack already removed or renamed the target
removeDeletes a matched child control from the arrayOften safer to hide a control via bindings or size than to remove it outright
RP/ui/hud_screen.json
{
  "namespace": "hud",

  "my_pack_custom_badge": {
    "type": "image",
    "texture": "textures/ui/my_pack/badge",
    "size": [16, 16],
    "anchor_from": "top_right",
    "anchor_to": "top_right",
    "offset": [-4, 4]
  },

  "root_panel": {
    "modifications": [
      {
        "array_name": "controls",
        "operation": "insert_back",
        "value": [
          {
            "[email protected]_pack_custom_badge": {}
          }
        ]
      }
    ]
  }
}

Isolate your work in custom namespaces

Every JSON UI file declares a namespace at the top, or inherits one from the file it is merging with. If you place twenty helper templates directly inside the hud or start_screen namespace using generic names like icon_panel or title_text, you risk colliding with either a future vanilla control or another creator's pack.

Keep your modification files tiny. Put all of your complex panel hierarchies, button templates, and animations into your own dedicated files registered in _ui_defs.json, under a namespace prefixed with your studio or pack identifier. In the vanilla screen file, reference your external template across namespaces using the @namespace.control_name inheritance syntax.

RP/ui/_ui_defs.json
{
  "ui_defs": [
    "ui/my_pack/common_templates.json",
    "ui/my_pack/custom_hud_panels.json"
  ]
}

Parameterize templates with $variables

JSON UI is an inheritance-driven language. Copy-pasting a fifty-line button or slot definition ten times makes your file hard to read and slow for the game to parse at screen load.

Define a single base control and expose the parts that change as $-prefixed variables with sensible default values. When you instantiate that template inside a controls array, pass only the variables that differ for that specific instance. Note that variables are resolved when the UI tree is built, not at runtime; if a value needs to change while the screen is open, it must be driven by a data binding instead of a variable.

RP/ui/my_pack/common_templates.json
{
  "namespace": "my_pack_common",

  "stat_row": {
    "type": "stack_panel",
    "orientation": "horizontal",
    "size": ["100%", 14],
    "$stat_label|default": "Stat",
    "$stat_texture|default": "textures/ui/WarningGlyph",
    "controls": [
      {
        "icon": {
          "type": "image",
          "texture": "$stat_texture",
          "size": [12, 12]
        }
      },
      {
        "padding": {
          "type": "panel",
          "size": [4, "100%"]
        }
      },
      {
        "label": {
          "type": "label",
          "text": "$stat_label",
          "color": "$body_text_color",
          "size": ["fill", "default"]
        }
      }
    ]
  }
}

Communicate with Behaviour Packs via title or form strings

JSON UI cannot read scoreboards, entity properties, or dynamic properties directly. It can only read the internal C++ data bindings that the current screen exposes. When you need to send custom gameplay data from a behaviour pack (commands or Script API) to the HUD or a form, the standard pattern is to encode a prefix tag into a string binding the screen already receives, such as #hud_title_text_string or #form_button_text.

Make your prefix distinct—for example, a short alphanumeric tag specific to your pack—and use string comparison in the target_property_name:#visible binding expression. Always hide or resize the vanilla title display only when your specific prefix is NOT present, or intercept it on a duplicate control so normal /title commands and other add-ons using the same channel continue to work.

RP/ui/my_pack/custom_hud_panels.json
{
  "namespace": "my_pack_hud",

  "mana_bar_panel": {
    "type": "panel",
    "size": [100, 12],
    "bindings": [
      {
        "binding_name": "#hud_title_text_string",
        "binding_name_override": "#title_text",
        "binding_type": "global"
      },
      {
        "binding_type": "view",
        "source_property_name": "(not ((#title_text - 'MYPACK_MANA:') = #title_text))",
        "target_property_name": "#visible"
      }
    ]
  }
}

Manage layering, relative sizing, and 9-slice textures

Screen resolutions and GUI scale sliders vary wildly across phones, consoles, and PCs. Avoid hardcoding pixel offsets from the edges of the screen to center an element. Instead, use anchor_from and anchor_to (such as center, bottom_middle, or top_right) combined with percentage sizes like "100%c" (fit to children) or "fill" inside a stack_panel.

Be deliberate with the layer property. Every child control inherits its parent's layer plus its own layer offset. If your custom panel renders behind a vanilla slot highlight or overlaps the chat screen, give your root container a modest layer offset rather than setting layer: 9999, which can cause your HUD element to draw on top of the pause menu or inventory tooltips.

When adding custom background panels, include a companion .json file right next to your .png texture in textures/ui/ defining nineslice_size. A 9-slice definition lets a tiny 16x16 border texture stretch cleanly to any panel size without blurring the corners.

Consider Ore UI migration before building large screens

Mojang is gradually replacing older JSON UI screens (such as the play screen, settings, and death screen) with a newer web-based interface framework internally called Ore UI. Once a vanilla screen is migrated to the new system, its old JSON UI file stops being used by the game.

Before investing weeks into a complex overhaul of a menu screen, check whether that screen is still driven by JSON UI in current Bedrock previews. For in-game menus driven by behaviour packs, prefer modifying server_form.json (used by ActionFormData and ModalFormData in the Script API) or the HUD, as these remain heavily relied upon by add-on creators.

Which versions this applies to

Applies to all modern Bedrock versions (1.16.0 through 1.21+). No experimental toggle is required, as JSON UI loads directly from any active Resource Pack. Note that individual vanilla screens are gradually being replaced by Ore UI in newer 1.21+ updates.

When it does not work

What you seeWhyThe fix
Your custom UI file is completely ignored and nothing changes on screen, with no error in the Content Log.New files inside RP/ui/ are not discovered automatically unless they share the exact path and filename of a vanilla screen. Standalone template files must be listed in RP/ui/_ui_defs.json.Create or edit RP/ui/_ui_defs.json and add the relative path (starting with "ui/...") to the "ui_defs" array.
Content Log shows: [UI][error] ... Can't find control 'my_control' during screen load, or the entire screen turns blank.You referenced a control across files without specifying its namespace, or there is a JSON syntax error in the file that defines that namespace, causing the entire file to fail parsing.Validate your JSON syntax first. Then check that any cross-file inheritance uses the full @namespace.control_name syntax and that the file defining that namespace is listed in _ui_defs.json.
A panel with a view binding stays visible all the time or never appears, even though the binding expression looks right.In JSON UI binding expressions, variable names (starting with $) are evaluated when the screen is constructed, whereas binding names (starting with #) are evaluated at runtime. Also, string subtraction tricks require exact case matching and single quotes around string literals.Ensure you have a data binding feeding the #property into the same control first, wrap string literals in single quotes inside source_property_name, and make sure target_property_name is set to "#visible".
Custom background textures look stretched and blurry at the corners when the panel resizes.The game stretches UI images uniformly unless it finds a 9-slice metadata file alongside the texture.Create a JSON file with the exact same name as your texture in the same folder (for example, RP/textures/ui/my_box.json naast my_box.png) and define "nineslice_size" and "base_size".

Reference, straight from the game files

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