How Sub-Block Detection Works
Bedrock's block JSON format only supports one `minecraft:selection_box` per block state. You cannot define multiple independent hitboxes inside a single block file and give each one its own event. However, when a player right-clicks or taps a block, the engine performs a raycast against that single selection box and calculates the exact point of impact.
Through the `@minecraft/server` Script API, custom components listening to the `onPlayerInteract` event receive a `BlockComponentPlayerInteractEvent` object. This object includes two critical pieces of data: `face`, which tells you which side of the block was hit, and `faceLocation`, a vector containing the X, Y, and Z offsets of the hit relative to the block's bottom-north-west corner. By checking which numerical range those coordinates fall into, your script can treat one block face as a grid of smaller buttons.
Understanding face and faceLocation
Every block occupies a 1x1x1 unit space in world coordinates. The `faceLocation` property returns values between `0.0` and `1.0` on each axis, always measured from the block's minimum corner (bottom, north, west) rather than the player's perspective. In Blockbench or pixel terms, multiplying a `faceLocation` axis by 16 gives you the exact pixel coordinate (`0` to `16`) where the cursor landed.
Because `faceLocation` is anchored to world directions, rotating your block with a cardinal direction trait changes which axis corresponds to 'left' and 'right' on the front of the model. If your block rotates, your script must read the block's direction permutation and adjust its coordinate math so the buttons stay aligned with the visual geometry.
| Property / Axis | Value Range | Behavior in World Space |
|---|---|---|
| face | Direction enum | Returns North, South, East, West, Up, or Down depending on which side of the selection box was hit. |
| faceLocation.x | 0.0 to 1.0 | Runs West (0.0) to East (1.0). Multiply by 16 to get the X pixel coordinate. |
| faceLocation.y | 0.0 to 1.0 | Runs Bottom (0.0) to Top (1.0). Multiply by 16 to get the Y pixel coordinate. Unaffected by horizontal rotation. |
| faceLocation.z | 0.0 to 1.0 | Runs North (0.0) to South (1.0). Multiply by 16 to get the Z pixel coordinate. |
Configuring the Block JSON
For `faceLocation` to feel accurate to the player, the block's `minecraft:selection_box` must wrap the visual mesh closely. If you make a flat wall-mounted panel using a custom geometry that is only 2 pixels thick, but you leave the default full-cube selection box, the player's cursor will hit the invisible outer boundary of the full block well before reaching the visual buttons. This creates parallax error where clicking a button at an angle triggers the wrong coordinate.
Below is a complete block definition for a two-drawer cabinet. It uses the `minecraft:placement_direction` trait so it faces the player when placed, defines a tight selection box, and attaches a custom component named `wiki:drawer_interaction` to handle the precision click logic.
{
"format_version": "1.21.10",
"minecraft:block": {
"description": {
"identifier": "wiki:drawer_cabinet",
"menu_category": {
"category": "items"
},
"traits": {
"minecraft:placement_direction": {
"enabled_states": [
"minecraft:cardinal_direction"
],
"y_rotation_offset": 180
}
},
"states": {
"wiki:top_open": [false, true],
"wiki:bottom_open": [false, true]
}
},
"components": {
"minecraft:geometry": "geometry.drawer_cabinet",
"minecraft:material_instances": {
"*": {
"texture": "drawer_cabinet",
"render_method": "opaque"
}
},
"minecraft:selection_box": {
"origin": [-8, 0, -8],
"size": [16, 16, 16]
},
"minecraft:collision_box": {
"origin": [-8, 0, -8],
"size": [16, 16, 16]
},
"minecraft:destructible_by_mining": {
"seconds_to_destroy": 2.0
},
"minecraft:custom_components": [
"wiki:drawer_interaction"
]
},
"permutations": [
{
"condition": "q.block_state('minecraft:cardinal_direction') == 'north'",
"components": {
"minecraft:transformation": { "rotation": [0, 0, 0] }
}
},
{
"condition": "q.block_state('minecraft:cardinal_direction') == 'south'",
"components": {
"minecraft:transformation": { "rotation": [0, 180, 0] }
}
},
{
"condition": "q.block_state('minecraft:cardinal_direction') == 'west'",
"components": {
"minecraft:transformation": { "rotation": [0, 90, 0] }
}
},
{
"condition": "q.block_state('minecraft:cardinal_direction') == 'east'",
"components": {
"minecraft:transformation": { "rotation": [0, 270, 0] }
}
}
]
}
}Reading Hit Coordinates in Script
In your behavior pack script, register the custom component during the `worldInitialize` event. When `onPlayerInteract` fires, first verify that the player actually clicked the front face of the cabinet by comparing the event's `face` against the block's `minecraft:cardinal_direction` state. Without this check, clicking the back or sides of the block at the right height would also open the drawers.
Vertical division is straightforward because `faceLocation.y` is unaffected by horizontal block rotation: anything below `0.5` (8 pixels high) is the bottom half, and `0.5` or above is the top half. For horizontal grids like a 4-button row, the script also demonstrates how to normalize `faceLocation.x` and `faceLocation.z` into a single `localX` value (`0.0` from left to `1.0` on the right) when looking straight at the front face.
import { world, Direction } from "@minecraft/server";
const FACE_TO_STATE = {
[Direction.North]: "north",
[Direction.South]: "south",
[Direction.East]: "east",
[Direction.West]: "west"
};
/**
* Converts world-space faceLocation into a normalized 0.0 (left) to 1.0 (right)
* horizontal coordinate across the front face of a cardinal-rotated block.
*/
function getLocalHorizontal(face, faceLocation) {
switch (face) {
case Direction.North:
return 1.0 - faceLocation.x;
case Direction.South:
return faceLocation.x;
case Direction.West:
return faceLocation.z;
case Direction.East:
return 1.0 - faceLocation.z;
default:
return 0.0;
}
}
world.beforeEvents.worldInitialize.subscribe((initEvent) => {
initEvent.blockComponentRegistry.registerCustomComponent("wiki:drawer_interaction", {
onPlayerInteract(event) {
const { block, face, faceLocation, player } = event;
if (!faceLocation || !player) return;
const permutation = block.permutation;
const facing = permutation.getState("minecraft:cardinal_direction");
// Only trigger if the player clicked the front face of the cabinet
if (FACE_TO_STATE[face] !== facing) {
return;
}
// Optional: get normalized left-to-right coordinate (0.0 to 1.0)
const localX = getLocalHorizontal(face, faceLocation);
// Ignore clicks on the outer 2-pixel wooden frame (2 / 16 = 0.125)
if (localX < 0.125 || localX > 0.875) {
return;
}
// Split vertically at the midpoint (8 pixels / 16 = 0.5)
if (faceLocation.y >= 0.5) {
const isOpen = permutation.getState("wiki:top_open");
block.setPermutation(permutation.withState("wiki:top_open", !isOpen));
player.playSound(isOpen ? "random.chestclosed" : "random.chestopen");
} else {
const isOpen = permutation.getState("wiki:bottom_open");
block.setPermutation(permutation.withState("wiki:bottom_open", !isOpen));
player.playSound(isOpen ? "random.chestclosed" : "random.chestopen");
}
}
});
});Reshaping Selection Boxes for Recessed Shapes
If your custom block is smaller than a full cube—such as a keypad mounted flat against a wall—you must shrink `minecraft:selection_box` in the base components so the raycast hits the actual surface of the model. Remember that when a permutation applies `minecraft:transformation` with a rotation, Bedrock automatically rotates both the visual geometry and the `minecraft:selection_box` together.
However, rotating the selection box does not rotate the coordinate system of `faceLocation`. If a thin wall panel is placed on a wall and rotated, the hit coordinates returned by `faceLocation` are still reported in fixed world space (`0.0` to `1.0` across the whole block cell). Keep your button bounds aligned to the model's pixel coordinates in Blockbench, divide those pixel bounds by `16`, and use the normalization helper shown above to account for the block's rotation.
Which versions this applies to
Applies to Minecraft Bedrock 1.21.0 and newer. Block custom components (`minecraft:custom_components`) and `faceLocation` on `BlockComponentPlayerInteractEvent` are stable in `@minecraft/server` 1.12.0+ and do not require any experimental toggles.
When it does not work
| What you see | Why | The fix |
|---|---|---|
| Content Log Error: [Blocks][error]-wiki:drawer_cabinet | minecraft:selection_box | box can't exceed (-8, 0, -8) to (8, 16, 8) | You tried to make a selection box larger than 16x16x16 pixels or positioned it outside the standard single-block boundary to create an oversized interactive panel. | Clamp your `origin` and `size` values so `origin` is no less than `[-8, 0, -8]` and `origin + size` does not exceed `[8, 16, 8]`. Multi-block structures require placing a second dummy block in the adjacent space. |
| Clicking the left button triggers the right button when the block is placed facing North or East. | `faceLocation.x` and `faceLocation.z` always increase toward East and South in world coordinates, not left-to-right across the block's rotated texture. | Invert the coordinate (`1.0 - faceLocation.x` or `1.0 - faceLocation.z`) depending on the `minecraft:cardinal_direction` state before checking which button was pressed. |
| Clicking a recessed button at an angle triggers the neighbour button instead. | The block's `minecraft:selection_box` is still set to a full 16x16x16 cube while the visual model is inset, so the raycast stops at the invisible outer boundary of the block cell. | Reduce the `size` and adjust the `origin` of `minecraft:selection_box` in the block JSON so its outer face sits flush with the interactive surface of your geometry. |
Reference, straight from the game files
Read out of the game itself, so the ids match the version you are building for.
