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.
| Operation | What it does | Notes |
|---|---|---|
| insert_back | Appends your controls to the end of the target array | Safest option for adding overlays; renders after earlier children |
| insert_front | Prepends your controls to the start of the target array | Useful when your element needs to sit behind existing children |
| insert_after | Places your controls immediately after a named child | Requires control_name or where to identify the target sibling |
| insert_before | Places your controls immediately before a named child | Requires control_name or where to identify the target sibling |
| replace | Swaps out a matched child control with your own | Fails silently if another pack already removed or renamed the target |
| remove | Deletes a matched child control from the array | Often safer to hide a control via bindings or size than to remove it outright |
{
"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.
{
"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.
{
"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.
{
"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 see | Why | The 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.
