Three.js Interaction
Use this skill for direct 3D input, object picking, camera manipulation, and selection
state. Route semantic HTML controls, keyboard accessibility, and focus management to
web-accessibility; route game-input architecture to web-game-development.
When to use this skill
- Raycast a pointer/touch against selectable scene objects
- Add or repair camera controls, object dragging, transform handles, or pointer lock
- Convert coordinates between client, normalized device, world, and screen space
- Fix missed clicks, stale hover state, pointer capture issues, or excessive raycasting
Instructions
Step 1: Define the interaction boundary
- Identify the canvas element and its
getBoundingClientRect(); never normalize pointer
coordinates against the full window when the canvas is offset or resized.
- Define an explicit pickable collection or layer. Raycasting
scene.children without
a boundary produces accidental targets and grows costly with scene size.
- Separate input events from visual state changes so a hover/selection can be tested and
cleared independently.
Step 2: Raycast from the actual canvas
const pointer = new THREE.Vector2();
const raycaster = new THREE.Raycaster();
function pick(event) {
const rect = renderer.domElement.getBoundingClientRect();
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
return raycaster.intersectObjects(pickables, true)[0] ?? null;
}
Resolve a nested hit to its semantic selectable root, rather than making every child mesh
a separate product-level target. Guard zero-sized canvases and ignore events outside the
canvas bounds.
Step 3: Choose controls and input behavior deliberately
| Requirement |
Appropriate tool |
| Orbit around a target |
OrbitControls |
| First-person browser view |
PointerLockControls |
| Map/pan constraints |
MapControls |
| Manipulate an object's transform |
TransformControls |
| Drag known objects |
DragControls or explicit ray-plane logic |
| Accessible command/UI action |
DOM controls plus web-accessibility |
Import controls from the installed Three.js add-ons path and dispose them when the
canvas/scene unmounts. Do not update controls in more than one render loop.
Step 4: Throttle work and clean state
Raycast on pointer movement only when the interaction requires hover. For large scenes,
use layers, bounds, broad-phase filtering, or a target list before considering more
complex acceleration structures. Clear hover state on pointerleave, lost capture,
object removal, and scene teardown.
Step 5: Verify input behavior
- Test mouse, touch, keyboard-mediated alternatives, and a resized/offset canvas.
- Test empty-space clicks, nested meshes, occluded objects, and rapid enter/leave events.
- Confirm controls release listeners and pointer lock/drag state on teardown.
- Verify focus-visible DOM affordances for every action that must work without a pointer.
Examples
Hover without material corruption
Keep each selectable's baseline visual state in a map. On hover change, restore the old
selection before applying the next highlight. Avoid mutating a shared material color when
only one mesh should appear highlighted; clone or use a supported per-object signal.
Place an object on a ground plane
Build a THREE.Plane, cast a ray from the pointer, and use ray.intersectPlane. This is
more predictable than guessing a depth from screen coordinates and works with any camera.
Best practices
- Normalize against the canvas rectangle, not
window.innerWidth and innerHeight.
- Keep semantic pick targets explicit and resolve child intersections to a root object.
- Use pointer events and pointer capture for drag interactions rather than separate mouse
and touch implementations.
- Dispose controls and remove custom listeners with the scene lifecycle.
- Do not make raycasting the only way to activate a necessary product action.
References
1---2name: threejs-interaction3description: Implement and debug Three.js interaction with Raycaster, pointer and touch coordinate conversion, hover/selection state, camera controls, dragging, keyboard input, and world-to-screen projection. Use when picking objects, adding OrbitControls or PointerLockControls, handling mouse/touch input, or fixing interactive 3D behavior. Triggers on: Three.js interaction, Raycaster, raycast, picking, click mesh, hover, object selection, OrbitControls, drag controls, pointer events, touch controls.4license: MIT5---67# Three.js Interaction89Use this skill for direct 3D input, object picking, camera manipulation, and selection10state. Route semantic HTML controls, keyboard accessibility, and focus management to11`web-accessibility`; route game-input architecture to `web-game-development`.1213## When to use this skill1415- Raycast a pointer/touch against selectable scene objects16- Add or repair camera controls, object dragging, transform handles, or pointer lock17- Convert coordinates between client, normalized device, world, and screen space18- Fix missed clicks, stale hover state, pointer capture issues, or excessive raycasting1920## Instructions2122### Step 1: Define the interaction boundary23241. Identify the canvas element and its `getBoundingClientRect()`; never normalize pointer25 coordinates against the full window when the canvas is offset or resized.262. Define an explicit pickable collection or layer. Raycasting `scene.children` without27 a boundary produces accidental targets and grows costly with scene size.283. Separate input events from visual state changes so a hover/selection can be tested and29 cleared independently.3031### Step 2: Raycast from the actual canvas3233```js34const pointer = new THREE.Vector2();35const raycaster = new THREE.Raycaster();3637function pick(event) {38 const rect = renderer.domElement.getBoundingClientRect();39 pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;40 pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;4142 raycaster.setFromCamera(pointer, camera);43 return raycaster.intersectObjects(pickables, true)[0] ?? null;44}45```4647Resolve a nested hit to its semantic selectable root, rather than making every child mesh48a separate product-level target. Guard zero-sized canvases and ignore events outside the49canvas bounds.5051### Step 3: Choose controls and input behavior deliberately5253| Requirement | Appropriate tool |54|---|---|55| Orbit around a target | `OrbitControls` |56| First-person browser view | `PointerLockControls` |57| Map/pan constraints | `MapControls` |58| Manipulate an object's transform | `TransformControls` |59| Drag known objects | `DragControls` or explicit ray-plane logic |60| Accessible command/UI action | DOM controls plus `web-accessibility` |6162Import controls from the installed Three.js add-ons path and dispose them when the63canvas/scene unmounts. Do not update controls in more than one render loop.6465### Step 4: Throttle work and clean state6667Raycast on pointer movement only when the interaction requires hover. For large scenes,68use layers, bounds, broad-phase filtering, or a target list before considering more69complex acceleration structures. Clear hover state on `pointerleave`, lost capture,70object removal, and scene teardown.7172### Step 5: Verify input behavior7374- Test mouse, touch, keyboard-mediated alternatives, and a resized/offset canvas.75- Test empty-space clicks, nested meshes, occluded objects, and rapid enter/leave events.76- Confirm controls release listeners and pointer lock/drag state on teardown.77- Verify focus-visible DOM affordances for every action that must work without a pointer.7879## Examples8081### Hover without material corruption8283Keep each selectable's baseline visual state in a map. On hover change, restore the old84selection before applying the next highlight. Avoid mutating a shared material color when85only one mesh should appear highlighted; clone or use a supported per-object signal.8687### Place an object on a ground plane8889Build a `THREE.Plane`, cast a ray from the pointer, and use `ray.intersectPlane`. This is90more predictable than guessing a depth from screen coordinates and works with any camera.9192## Best practices93941. Normalize against the canvas rectangle, not `window.innerWidth` and `innerHeight`.952. Keep semantic pick targets explicit and resolve child intersections to a root object.963. Use pointer events and pointer capture for drag interactions rather than separate mouse97 and touch implementations.984. Dispose controls and remove custom listeners with the scene lifecycle.995. Do not make raycasting the only way to activate a necessary product action.100101## References102103- [Three.js Interaction source coverage](https://github.com/CloudAI-X/threejs-skills/tree/main/skills/threejs-interaction)104- [Raycaster documentation](https://threejs.org/docs/#api/en/core/Raycaster)105- [Three.js manual: picking](https://threejs.org/manual/#en/picking)