How Custom Fences Work in Bedrock
Vanilla fences rely on hardcoded internal logic to detect neighbors, expand their collision height to 1.5 blocks, and accept lead knots. When building a custom fence in a behavior pack, you replicate the visual connection logic yourself by combining a multi-bone Blockbench model with four boolean block states representing the four cardinal directions.
The core concept is straightforward: your block model contains one central post bone that is always visible, plus four arm bones (`north`, `south`, `east`, `west`). Using `bone_visibility` inside the `minecraft:geometry` component, you hide or show each arm depending on the block's current state. A small Script API handler listens for block placement and neighbor updates to toggle those states automatically.
| State or Bone | Type / Role | Notes |
|---|---|---|
| wiki:north | Boolean State | True when a connectable block is at z - 1 |
| wiki:south | Boolean State | True when a connectable block is at z + 1 |
| wiki:east | Boolean State | True when a connectable block is at x + 1 |
| wiki:west | Boolean State | True when a connectable block is at x - 1 |
| post | Geometry Bone | Central 4x16x4 pillar, always visible |
Preparing the Fence Geometry
In Blockbench, create a new Bedrock Block model and set the identifier to `geometry.custom_fence`. Start by creating a bone named `post` centered at `[0, 0, 0]` containing a cube that spans from `[-2, 0, -2]` to `[2, 16, 2]` in model space (which corresponds to a 4x16x4 pixel column in the center of the block).
Next, create four separate bones named `north`, `south`, `east`, and `west`. The `north` bone should contain the upper and lower horizontal bars extending from the north face of the post (`z = -2`) to the north edge of the block (`z = -8`). Repeat this for the remaining three directions. Keeping each direction in its own top-level bone allows the block definition to toggle them independently without needing separate geometry files for every possible permutation.
Block Behavior Definition
The behavior pack JSON defines the four directional states, assigns the custom geometry with Molang visibility expressions, and registers a custom component name so your script knows which blocks to manage. Note that `minecraft:collision_box` and `minecraft:selection_box` are constrained to a maximum height of 16 pixels (one full block) in current stable releases, so custom fences cannot naturally have the 24-pixel tall collision box that stops players from jumping over vanilla fences.
If you want the selection and collision boxes to shrink and grow with the arms, you can add `permutations` that override `minecraft:collision_box` and `minecraft:selection_box`. However, because `minecraft:collision_box` only accepts a single cube (`origin` and `size`), most creators either leave the collision box at the central post (`[-2, 0, -2]` with size `[4, 16, 4]`) or expand it slightly to `[8, 16, 8]` as a compromise.
{
"format_version": "1.21.10",
"minecraft:block": {
"description": {
"identifier": "wiki:cedar_fence",
"menu_category": {
"category": "construction",
"group": "itemGroup.name.fence"
},
"states": {
"wiki:north": [false, true],
"wiki:south": [false, true],
"wiki:east": [false, true],
"wiki:west": [false, true]
}
},
"components": {
"minecraft:geometry": {
"identifier": "geometry.custom_fence",
"bone_visibility": {
"post": true,
"north": "q.block_state('wiki:north')",
"south": "q.block_state('wiki:south')",
"east": "q.block_state('wiki:east')",
"west": "q.block_state('wiki:west')"
}
},
"minecraft:material_instances": {
"*": {
"texture": "cedar_planks",
"render_method": "alpha_test"
}
},
"minecraft:collision_box": {
"origin": [-4, 0, -4],
"size": [8, 16, 8]
},
"minecraft:selection_box": {
"origin": [-4, 0, -4],
"size": [8, 16, 8]
},
"minecraft:light_dampening": 0,
"minecraft:destructible_by_mining": {
"seconds_to_destroy": 2.0
},
"minecraft:custom_components": [
"wiki:fence_connect"
],
"tag:wood": {}
}
}
}Updating Connections with Script API
Because JSON block definitions cannot read the block states or identifiers of adjacent blocks on their own, you use `@minecraft/server` to update the directional states. While registering the `wiki:fence_connect` custom component handles initial placement via `onPlace`, custom components do not currently expose a direct neighbor-update callback. To make existing fences react when a block is placed or broken next to them, combine the custom component with `world.afterEvents.playerPlaceBlock` and `world.afterEvents.playerBreakBlock`.
The helper function below checks all four horizontal neighbors of a fence block. If a neighbor is not air or liquid, it sets the corresponding state to `true`. It can also trigger a second pass on adjacent custom fences so both sides of a new connection snap together in the same tick.
import { world } from "@minecraft/server";
const IGNORED_BLOCKS = new Set([
"minecraft:air",
"minecraft:water",
"minecraft:flowing_water",
"minecraft:lava",
"minecraft:flowing_lava",
"minecraft:short_grass",
"minecraft:tall_grass"
]);
function canConnect(block) {
if (!block) return false;
return !IGNORED_BLOCKS.has(block.typeId);
}
function updateFence(block, updateNeighbors = false) {
if (!block || block.typeId !== "wiki:cedar_fence") return;
const north = block.north();
const south = block.south();
const east = block.east();
const west = block.west();
const newPerm = block.permutation
.withState("wiki:north", canConnect(north))
.withState("wiki:south", canConnect(south))
.withState("wiki:east", canConnect(east))
.withState("wiki:west", canConnect(west));
block.setPermutation(newPerm);
if (updateNeighbors) {
updateFence(north, false);
updateFence(south, false);
updateFence(east, false);
updateFence(west, false);
}
}
world.beforeEvents.worldInitialize.subscribe((event) => {
event.blockComponentRegistry.registerCustomComponent("wiki:fence_connect", {
onPlace(e) {
updateFence(e.block, true);
}
});
});
world.afterEvents.playerPlaceBlock.subscribe((event) => {
const b = event.block;
updateFence(b.north(), false);
updateFence(b.south(), false);
updateFence(b.east(), false);
updateFence(b.west(), false);
});
world.afterEvents.playerBreakBlock.subscribe((event) => {
const b = event.block;
updateFence(b.north(), false);
updateFence(b.south(), false);
updateFence(b.east(), false);
updateFence(b.west(), false);
});
Known Limitations Compared to Vanilla Fences
Even with custom geometry and scripting, custom blocks cannot replicate three hardcoded vanilla fence behaviors without workarounds. First, `minecraft:collision_box` rejects any `origin` and `size` combination that extends past 16 pixels on the Y axis, meaning players and mobs can jump over a custom fence unless you place an invisible barrier-like block above it or use an entity.
Second, vanilla wooden and nether brick fences will not extend their arms toward your custom block because their connection rules check an internal hardcoded tag list. Third, leads cannot be tied directly to custom fence blocks unless you spawn a tiny invisible entity inside the post to accept the lead interaction.
Which versions this applies to
Applies to Bedrock Edition 1.21.0 and newer. Block `bone_visibility`, `minecraft:custom_components`, and the `@minecraft/server` methods used here are stable and require no experimental toggles.
When it does not work
| What you see | Why | The fix |
|---|---|---|
| Content Log Error: [Blocks] wiki:cedar_fence | minecraft:collision_box: box must be contained within the block unit cube | You tried to set the Y size of `minecraft:collision_box` to 24 (1.5 blocks tall) to stop players from jumping over the fence like a vanilla fence. | Clamp the Y origin and size so their sum does not exceed 16, for example `"origin": [-4, 0, -4]` and `"size": [8, 16, 8]`. |
| Fence arms never appear even though the script runs without errors. | The bone names inside your `.geo.json` file do not match the keys under `bone_visibility`, or your Molang string uses `q.block_property` instead of `q.block_state`. | Open the geometry file in Blockbench to confirm the bones are named `north`, `south`, `east`, and `west` in lowercase, and verify `bone_visibility` uses `q.block_state('wiki:north')`. |
| Adjacent blocks touching the fence have invisible faces (X-ray effect into the ground). | The engine treats custom blocks as opaque full cubes for face culling unless told otherwise. | Ensure `minecraft:geometry` is using a non-full-cube model, set `"minecraft:light_dampening": 0`, and use `"render_method": "alpha_test"` or `"opaque"` on a properly sized geometry. |
Reference, straight from the game files
Read out of the game itself, so the ids match the version you are building for.
