The button you wrote in the last chapter handled an onClick from the DOM.
solid-three lets you put the same handler on a <T.Mesh> — and gives it
the right semantics for a 3D world: it only fires when the click actually
hits that mesh.
Click the cube. Same toggle as before — just the target moved into the scene.
Under the hood, solid-three runs a Raycaster against the pointer's
position each event, finds which meshes the ray hits, and fires the
corresponding handler on the nearest one. You don't have to wire any of
that — adding onClick is enough.
Hover
onPointerEnter and onPointerLeave work the same way: they fire when
the ray first enters or finally leaves a mesh. Combine them with a signal
to react to hover.
Move the pointer over the cube — it grows. Leave — it shrinks. The signal
flips; scale reads it; one property assignment runs.
Clicking nothing
Sometimes the interesting event is when the user clicks but doesn't hit any
mesh — to deselect the current selection, dismiss a tooltip, that sort of
thing. <Canvas> exposes onClickMissed for exactly that case:
Click the cube to select it; click anywhere else in the canvas to deselect. The two handlers cover the full "did you hit something?" question between them.
That covers the everyday cases. There's a
longer list of pointer events
(onPointerMove, onPointerDown, onPointerUp, onWheel, ...) — all
following the same convention.
Stopping events
When you click in a scene, the ray usually hits more than one mesh — the
front cube, the back cube, and whatever else happens to be in line. By
default, solid-three fires the same event on every hit, from the closest
to the furthest. Each handler can choose to "consume" the event by calling
event.stopPropagation(), just like a DOM event.
Click the small front cube — only it changes colour, because its handler
called event.stopPropagation(). Click the larger background cube on either
side of the front one — it changes colour, and there's nothing in the way to
absorb the event.
Take the event.stopPropagation() line out and click the front cube again:
both cubes toggle. The event walked through everything the ray hit.
When you actually want the full list
Stopping is the common case, but sometimes you want to know about every hit
— measuring through walls, x-ray selection, "click through" tools. The event
exposes the full intersections array (sorted nearest-first), so a single
handler can deal with the whole stack itself:
<T.Mesh onClick={event => { for (const hit of event.intersections) { console.log(hit.distance, hit.object.name) }}}>Most scenes never need this — but it's there when you do.
The handler receives an enriched event object — the original DOM event
plus the three.js intersection data — useful any time "click" alone
isn't enough (drag-to-rotate, paint-on-surface, world-space gizmos).
The events API reference has the full
event-object shape and the complete handler list.
Dragging
A click fires once. Hover fires as the ray crosses a mesh. A drag is different: it needs every move to keep reaching the same object — even after the pointer slides off it. Left to the default, the moves would jump to whatever is under the pointer now, and the gesture would fall apart the moment your aim drifts.
setPointerCapture() solves it. Call it from onPointerDown and that
pointer is captured: every following onPointerMove and onPointerUp
goes to this mesh — on the ray or off it — until you let go.
Grab the cube and drag. It follows the pointer, and keeps following even
when the pointer races ahead of it or leaves the canvas. On pointerup
the capture releases on its own.
One detail makes it feel right: the event.stopPropagation() in
onPointerDown keeps the press from also grabbing meshes stacked behind the
cube — you grab the front one, not the whole column.
So far every trigger has been a one-off — a click, a hover-in, a hover-out. The next chapter introduces the third kind: time.
Last updated: 6/8/26, 11:20 AM