Wiki 9Minecraft Wiki 9Minecraft

Creating Custom Trapdoors

Build a working trapdoor using block states, placement traits, permutations, and the Script API.

5Sections
3Code examples
3Known errors

A trapdoor is an interactive block that flips between a flat horizontal state and a vertical open state, mounted on either the top or bottom half of a block space. Because Minecraft Bedrock does not expose a native trapdoor component to add-ons, creators build them from scratch by combining placement traits, block permutations, and a small Script API custom component. This guide walks through assembling all the required files so your custom trapdoor places, rotates, collides, and toggles just like a vanilla one.

How Bedrock Trapdoors Work

When you place a vanilla trapdoor, the game tracks three separate pieces of data: which cardinal direction the hinges face, whether the block sits at the bottom or top of the grid space, and whether the flap is currently swung open. In a custom block, the first two pieces are handled automatically by the placement_direction and placement_position block traits. The third piece—the open or closed state—must be defined as a custom boolean state in your block description.

Once those three states exist on the block, permutations inspect them through Molang queries and swap out the geometry, transformation rotation, and collision box to match. Finally, because legacy JSON block events were removed in modern format versions, a Script API custom component listens for player interaction and flips the custom open state between true and false.

State or TraitValuesPurpose
minecraft:cardinal_directionnorth, south, east, westAdded by the placement_direction trait. Tracks which way the player was looking when placing the block.
minecraft:vertical_halfbottom, topAdded by the placement_position trait. Tracks whether the player clicked the upper or lower half of a face.
wiki:openfalse, trueCustom boolean state defined in your block JSON. Toggled via script when a player interacts with the block.

Geometry and Textures

You only need two geometry models to handle every orientation of a trapdoor, provided you design them carefully around the block center. The simplest approach is to create one model lying flat at the bottom of the block space (16x3x16 pixels) for the closed state, and a second model standing vertically against the north edge (16x16x3 pixels) for the open state. The block definition can then rotate these two models on the X and Y axes to cover all eight combinations of direction and vertical half.

Make sure the UVs on your top and bottom faces line up cleanly when the block is rotated, or your hinge texture will appear on the wrong edge when mounted to the top half of a block. Place the geometry file inside your Resource Pack models folder and register your textures in terrain_texture.json as you would for any standard custom block.

RP/models/blocks/custom_trapdoor.geo.json
{
  "format_version": "1.12.0",
  "minecraft:geometry": [
    {
      "description": {
        "identifier": "geometry.custom_trapdoor_closed",
        "texture_width": 16,
        "texture_height": 16,
        "visible_bounds_width": 2,
        "visible_bounds_height": 2,
        "visible_bounds_offset": [0, 0.5, 0]
      },
      "bones": [
        {
          "name": "trapdoor",
          "pivot": [0, 0, 0],
          "cubes": [
            {
              "origin": [-8, 0, -8],
              "size": [16, 3, 16],
              "uv": {
                "north": {"uv": [0, 13], "uv_size": [16, 3]},
                "east": {"uv": [0, 13], "uv_size": [16, 3]},
                "south": {"uv": [0, 13], "uv_size": [16, 3]},
                "west": {"uv": [0, 13], "uv_size": [16, 3]},
                "up": {"uv": [0, 0], "uv_size": [16, 16]},
                "down": {"uv": [0, 0], "uv_size": [16, 16]}
              }
            }
          ]
        }
      ]
    },
    {
      "description": {
        "identifier": "geometry.custom_trapdoor_open",
        "texture_width": 16,
        "texture_height": 16,
        "visible_bounds_width": 2,
        "visible_bounds_height": 2,
        "visible_bounds_offset": [0, 0.5, 0]
      },
      "bones": [
        {
          "name": "trapdoor",
          "pivot": [0, 0, 0],
          "cubes": [
            {
              "origin": [-8, 0, 5],
              "size": [16, 16, 3],
              "uv": {
                "north": {"uv": [0, 0], "uv_size": [16, 16]},
                "east": {"uv": [0, 0], "uv_size": [3, 16]},
                "south": {"uv": [0, 0], "uv_size": [16, 16]},
                "west": {"uv": [13, 0], "uv_size": [3, 16]},
                "up": {"uv": [0, 0], "uv_size": [16, 3]},
                "down": {"uv": [0, 13], "uv_size": [16, 3]}
              }
            }
          ]
        }
      ]
    }
  ]
}

Block Definition and Permutations

In the Behavior Pack block file, enable both placement_direction and placement_position under the traits object. For placement_direction, use the cardinal_direction state and set y_rotation_offset to 180 so the hinge attaches to the block behind the trapdoor relative to the player. Under states, declare your boolean open state with false listed first so newly placed trapdoors start closed.

Because minecraft:transformation cannot be defined twice in two active permutations at the same time, you cannot use one permutation for horizontal facing and a separate overlapping permutation to flip the model upside down for the top half. For the closed state, the cleanest approach is to keep the geometry upright and simply move the collision and selection boxes to the top 3 pixels of the block space, or use an upper closed geometry if you skip transformation on the vertical half. Even simpler: use explicit permutations that combine the cardinal direction with the vertical half or open state so only one permutation with a transformation component matches at any given moment.

Always use the cutout render method in minecraft:material_instances if your trapdoor texture has transparent holes, and drop minecraft:light_dampening to 0 so the block does not cast pitch-black shadows onto the blocks beneath it.

BP/blocks/custom_trapdoor.json
{
  "format_version": "1.21.10",
  "minecraft:block": {
    "description": {
      "identifier": "wiki:custom_trapdoor",
      "menu_category": {
        "category": "construction",
        "group": "itemGroup.name.trapdoor"
      },
      "traits": {
        "minecraft:placement_direction": {
          "enabled_states": ["minecraft:cardinal_direction"],
          "y_rotation_offset": 180.0
        },
        "minecraft:placement_position": {
          "enabled_states": ["minecraft:vertical_half"]
        }
      },
      "states": {
        "wiki:open": [false, true]
      }
    },
    "components": {
      "minecraft:destructible_by_mining": {
        "seconds_to_destroy": 3.0
      },
      "minecraft:destructible_by_explosion": {
        "explosion_resistance": 3.0
      },
      "minecraft:light_dampening": 0,
      "minecraft:geometry": "geometry.custom_trapdoor_closed",
      "minecraft:material_instances": {
        "*": {
          "texture": "custom_trapdoor",
          "render_method": "alpha_test"
        }
      },
      "minecraft:custom_components": [
        "wiki:trapdoor_interact"
      ]
    },
    "permutations": [
      {
        "condition": "!q.block_state('wiki:open') && q.block_state('minecraft:vertical_half') == 'bottom'",
        "components": {
          "minecraft:geometry": "geometry.custom_trapdoor_closed",
          "minecraft:collision_box": {
            "origin": [-8, 0, -8],
            "size": [16, 3, 16]
          },
          "minecraft:selection_box": {
            "origin": [-8, 0, -8],
            "size": [16, 3, 16]
          }
        }
      },
      {
        "condition": "!q.block_state('wiki:open') && q.block_state('minecraft:vertical_half') == 'top'",
        "components": {
          "minecraft:geometry": "geometry.custom_trapdoor_closed",
          "minecraft:collision_box": {
            "origin": [-8, 0, -8],
            "size": [16, 3, 16]
          },
          "minecraft:selection_box": {
            "origin": [-8, 0, -8],
            "size": [16, 3, 16]
          }
        }
      },
      {
        "condition": "q.block_state('wiki:open')",
        "components": {
          "minecraft:geometry": "geometry.custom_trapdoor_open",
          "minecraft:collision_box": {
            "origin": [-8, 0, 5],
            "size": [16, 16, 3]
          },
          "minecraft:selection_box": {
            "origin": [-8, 0, 5],
            "size": [16, 16, 3]
          }
        }
      },
      {
        "condition": "q.block_state('minecraft:cardinal_direction') == 'north' && (q.block_state('wiki:open') || q.block_state('minecraft:vertical_half') == 'bottom')",
        "components": {
          "minecraft:transformation": { "rotation": [0, 0, 0] }
        }
      },
      {
        "condition": "q.block_state('minecraft:cardinal_direction') == 'south' && (q.block_state('wiki:open') || q.block_state('minecraft:vertical_half') == 'bottom')",
        "components": {
          "minecraft:transformation": { "rotation": [0, 180, 0] }
        }
      },
      {
        "condition": "q.block_state('minecraft:cardinal_direction') == 'east' && (q.block_state('wiki:open') || q.block_state('minecraft:vertical_half') == 'bottom')",
        "components": {
          "minecraft:transformation": { "rotation": [0, 270, 0] }
        }
      },
      {
        "condition": "q.block_state('minecraft:cardinal_direction') == 'west' && (q.block_state('wiki:open') || q.block_state('minecraft:vertical_half') == 'bottom')",
        "components": {
          "minecraft:transformation": { "rotation": [0, 90, 0] }
        }
      },
      {
        "condition": "q.block_state('minecraft:cardinal_direction') == 'north' && !q.block_state('wiki:open') && q.block_state('minecraft:vertical_half') == 'top'",
        "components": {
          "minecraft:transformation": {
            "rotation": [0, 0, 0],
            "translation": [0, 0.8125, 0]
          }
        }
      },
      {
        "condition": "q.block_state('minecraft:cardinal_direction') == 'south' && !q.block_state('wiki:open') && q.block_state('minecraft:vertical_half') == 'top'",
        "components": {
          "minecraft:transformation": {
            "rotation": [0, 180, 0],
            "translation": [0, 0.8125, 0]
          }
        }
      },
      {
        "condition": "q.block_state('minecraft:cardinal_direction') == 'east' && !q.block_state('wiki:open') && q.block_state('minecraft:vertical_half') == 'top'",
        "components": {
          "minecraft:transformation": {
            "rotation": [0, 270, 0],
            "translation": [0, 0.8125, 0]
          }
        }
      },
      {
        "condition": "q.block_state('minecraft:cardinal_direction') == 'west' && !q.block_state('wiki:open') && q.block_state('minecraft:vertical_half') == 'top'",
        "components": {
          "minecraft:transformation": {
            "rotation": [0, 90, 0],
            "translation": [0, 0.8125, 0]
          }
        }
      }
    ]
  }
}

Toggling State with a Custom Component

Notice how the top closed permutations in the block file use a translation of 0.8125 on the Y axis inside minecraft:transformation. Because one block unit equals 16 pixels, shifting a 3-pixel-tall model up by 13 pixels (13 divided by 16 is 0.8125) pushes both the visual geometry and its collision box flush against the top of the block space without needing a third geometry file.

To make the block respond when clicked, register a block custom component during the worldInitialize startup event in your behavior pack script. When onPlayerInteract fires, read the current value of the wiki:open state from the block permutation, invert it, apply the updated permutation back to the block, and play a sound across the dimension.

BP/scripts/main.js
import { world } from "@minecraft/server";

world.beforeEvents.worldInitialize.subscribe((event) => {
  event.blockComponentRegistry.registerCustomComponent("wiki:trapdoor_interact", {
    onPlayerInteract(e) {
      const { block, dimension } = e;
      const permutation = block.permutation;
      const isOpen = permutation.getState("wiki:open");

      block.setPermutation(permutation.withState("wiki:open", !isOpen));

      const soundId = isOpen ? "close.wooden_trapdoor" : "open.wooden_trapdoor";
      dimension.playSound(soundId, block.center());
    }
  });
});

Waterlogging and Redstone Limitations

Unlike vanilla trapdoors, custom blocks in Bedrock currently lack a built-in JSON component for listening to adjacent redstone dust or redstone torches. If you need your custom trapdoor to open from a lever or button, you must poll neighbouring blocks or listen to block update events in the Script API, which can add overhead if hundreds of trapdoors exist in an area.

Waterlogging works out of the box whenever a block's collision box does not fill the entire 16x16x16 volume. Players can place water directly into the same block space as your trapdoor without extra components, though water will flow out of the sides unless surrounded by solid faces.

Which versions this applies to

Applies to Minecraft Bedrock 1.21.10 and newer. Block traits, permutations, and custom components are stable features and do not require any experimental toggles in world settings. Ensure your behavior pack manifest.json includes a script module dependency on @minecraft/server version 1.12.0 or higher.

When it does not work

What you seeWhyThe fix
[Blocks][error]-wiki:custom_trapdoor | Permutation has duplicate component 'minecraft:transformation'Two different permutations evaluated to true at the same time and both attempted to apply a minecraft:transformation component (for example, one permutation handling cardinal direction and another handling the top vertical half).Combine the Molang conditions using && so that only a single permutation containing minecraft:transformation can ever be active at once.
Failed to find custom component 'wiki:trapdoor_interact'The game loaded the block JSON before the script registered the custom component, or the script failed to run due to a missing @minecraft/server dependency in manifest.json.Register the component inside world.beforeEvents.worldInitialize (or system.beforeEvents.startup in newer API versions) and verify that your script entry point is listed properly in BP/manifest.json.
Collision box extends outside the valid block boundsTranslating or rotating a geometry via minecraft:transformation also moves its collision_box and selection_box. If your collision box is already at the top of the block and you apply a positive Y translation, the box gets pushed outside the 16x16x16 block boundary.Keep the base collision_box origin at [-8, 0, -8] when using transformation translation to shift the top-half closed trapdoor upward.

Reference, straight from the game files

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