Course outline
Events in R3F
In one line
Meshes take onClick and onPointerOver props. R3F raycasts for you and calls the handler of every object under the pointer, nearest first, until one calls stopPropagation(). Keyboard input comes from drei's KeyboardControls.
Think of it like this
A crowd in a stairwell. You shout "fire!" and the person nearest hears it first. They can pass it on, or they can say "I have it, stay calm" and stop it there. stopPropagation() is that second sentence.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- pointer
- A mouse, a finger or a pen. The pointer events cover all three with one set of names.
- bubbling
- An event reaching one handler after another, from the innermost object outwards. In R3F, from the nearest hit to the farthest.
- stopPropagation
- A method on the event that stops it reaching the next handler.
- intersection
- One place where the pointer's ray hits an object, with its distance and point.
- event source
- The HTML element the Canvas listens on. By default it is the Canvas's own wrapper.
Step by step
01Two things on one line of sight
The crate (a 1 m cube at z = 1) hides the middle of the wall (z = -1) from a camera at z = 4. Both have onClick.
02Send a real click
Clicker waits for frame 5, sends pointerdown, pointerup and click to the middle of the canvas, and does it again at frame 8 with stop turned on.
03Read the order
Click 1 logs the crate, then the wall. Click 2 logs only the crate. The distances are the real ones: 4 - 1.5 = 2.50 to the cube's front, and 5 to the wall.
04Keys
The second example maps ArrowLeft and KeyA to left. At frame 5 a key goes down and at frame 10 it goes up. The log shows both changes.
Try it yourself
- 1
Stop from the start
In the first example, set
settings = { stop: true }. Both clicks log only the crate: the wall never answers. - 2
Drop the pointerdown
Delete the line that sends
pointerdown. Neither click calls any handler: the output is only the twoclick Nheadings. R3F needs the press to know which objects the click began on. - 3
Drag before releasing
Send
pointerupandclick20 pixels to the right:const moved = { ...at, clientX: at.clientX + 20 };and usemovedfor both. Add"| delta", event.deltato the crate'sconsole.log. The crate and the wall are still called, and the line ends| delta 20(the distances become 2.51 and 5.01, since the release point moved). A click handler must checkevent.deltaitself. - 4
Use the keys
In the second example, press the left arrow while the page has focus. The box turns anticlockwise as long as you hold the key.
Code & diagrams
Two clicks at the same point. In the second, the crate stops the event.
Console
Run the scene to see its console.Expected console output
THREE.Clock: This module has been deprecated. Please use THREE.Timer instead.
click 1, nothing stops it:
crate clicked, distance 2.50 | hits under the pointer: 2
wall clicked, distance 5.00
click 2, the crate calls stopPropagation():
crate clicked, distance 2.50 | hits under the pointer: 2Arrow keys or A and D turn the box (click the canvas area first if the page does not have focus). The log is printed when `left` changes.
Console
Run the scene to see its console.Expected console output
THREE.Clock: This module has been deprecated. Please use THREE.Timer instead.
left is now true | turned anticlockwise so far: false
left is now false | turned anticlockwise so far: trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
The key that never comes up
In the second example, delete the line that sends keyup.
Myth vs fact
Myth
A click on an object in front stops the object behind from getting it.
Fact
Not unless you call stopPropagation(). By default every object under the pointer is called, nearest first.
Myth
Pointer events need no keyboard version.
Fact
A person who cannot use a mouse needs to turn, pick and choose with keys. Give every pointer action a key or a button (Topics 8.4 and 18.3).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
onPointerMoveon a large mesh runs a raycast against every triangle of it on every pointer move. Add aBvh(drei) for big meshes, or handle moves on a simple proxy mesh. - ▸
Handlers fire in distance order, but only for objects that were hit. A transparent material still blocks events unless you set
raycast={() => null}on the mesh. - ▸
Pointer capture:
event.target.setPointerCapture(event.pointerId)inonPointerDownsends later moves to the same object even after the pointer leaves it. It is how drag handles stay under the finger.
Remember this
- 1
The problem this solves: in plain three.js you write the raycaster yourself (Phase 8): convert the pointer to coordinates, cast a ray, read the hits. In R3F the same work is done for you, and the result arrives as the prop you expect.
- 2
The handlers.
onClick,onDoubleClick,onContextMenu,onPointerDown,onPointerUp,onPointerOver,onPointerOut,onPointerMove,onWheelandonPointerMissed(a click that hit nothing; it is a prop on<Canvas>and on meshes). Only objects with a handler are tested. - 3
The event object. It has
object(the object that was hit),eventObject(the object the handler is on),point(the world position hit),distance(from the camera),intersections(every hit under the pointer, nearest first), plus the browser's ownnativeEvent. The first example printsevent.distanceand the number of intersections. - 4
**Order and
stopPropagation.** The ray usually hits several objects. R3F calls the handler of the nearest first, then the next, in order of distance, like a bubbling event. In the first example, click 1 calls the crate (distance 2.50) and then the wall (distance 5.00). In click 2 the crate callsevent.stopPropagation(), so the wall is never called.intersectionsstill lists both, because it describes what is under the pointer, not who was told. - 5
Click or drag? R3F remembers which objects were under the pointer at
pointerdownand callsonClickonly on those. It does not drop a click after a drag: the handler still runs, withevent.delta, the pixels between press and release. Drag the camera with OrbitControls and let go over a part, and it is "clicked" unless you ignore it:if (event.delta > 2) return;(Topic 8.2). The example sendspointerdown,pointerupandclickin that order; with no earlierpointerdown, no handler runs. - 6
Keyboard. Pointer events work only on objects; keys belong to the page. drei's
<KeyboardControls map={[...]}>names keys ({ name: "left", keys: ["ArrowLeft", "KeyA"] }) anduseKeyboardControls()returns[subscribe, get]. It listens onwindowunless you passdomElement, so arrow keys typed in a text box on the page also turn the object.get()is a snapshot of every named key, safe to read inuseFramewith no re-render. This is the keyboard twin of dragging, for people who cannot use a pointer (Topics 8.4 and 18.3). - 7
Cost and cursor. Raycasting runs on pointer moves only for objects with handlers, against their bounding spheres first. Many handlers on a large mesh is slow: put one handler on a parent group and read
event.object. Change the cursor inonPointerOver:document.body.style.cursor = "pointer", and restore it inonPointerOut.
Explain it without notes
A customer clicks the saddle and the frame behind it also reacts. Why, and how do you stop it?
A customer orbits the bike by dragging, and the part under the pointer gets selected when they let go. Why, and what is the fix?
Practice
Write a mesh that turns pink on hover and back to orange when the pointer leaves, using a ref and no state.
Map Space to pause with KeyboardControls and read it inside useFrame to stop a rotation.
Done when you can
Add pointer handlers and read the event
Say the order of handlers and stop it
Ignore a drag in onClick with event.delta
Add keyboard controls as a twin for pointer actions