Command Palette

Search for a command to run...

Hectal
Course outline
Topic 11.4Intermediate ~21 min · step 4 of 7

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.

0/7 · 0%

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. 1

    Stop from the start

    In the first example, set settings = { stop: true }. Both clicks log only the crate: the wall never answers.

  2. 2

    Drop the pointerdown

    Delete the line that sends pointerdown. Neither click calls any handler: the output is only the two click N headings. R3F needs the press to know which objects the click began on.

  3. 3

    Drag before releasing

    Send pointerup and click 20 pixels to the right: const moved = { ...at, clientX: at.clientX + 20 }; and use moved for both. Add "| delta", event.delta to the crate's console.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 check event.delta itself.

  4. 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

A click that hits two meshes New tab

Two clicks at the same point. In the second, the crate stops the event.

idle

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: 2
The keyboard twin New tab

Arrow 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.

idle

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: true

Break 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.

terminal
$ Run
── what you'll see ──
left is now true | turned anticlockwise so far: false

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.

  • ▸

    onPointerMove on a large mesh runs a raycast against every triangle of it on every pointer move. Add a Bvh (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) in onPointerDown sends later moves to the same object even after the pointer leaves it. It is how drag handles stay under the finger.

Remember this

  1. 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. 2

    The handlers. onClick, onDoubleClick, onContextMenu, onPointerDown, onPointerUp, onPointerOver, onPointerOut, onPointerMove, onWheel and onPointerMissed (a click that hit nothing; it is a prop on <Canvas> and on meshes). Only objects with a handler are tested.

  3. 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 own nativeEvent. The first example prints event.distance and the number of intersections.

  4. 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 calls event.stopPropagation(), so the wall is never called. intersections still lists both, because it describes what is under the pointer, not who was told.

  5. 5

    Click or drag? R3F remembers which objects were under the pointer at pointerdown and calls onClick only on those. It does not drop a click after a drag: the handler still runs, with event.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 sends pointerdown, pointerup and click in that order; with no earlier pointerdown, no handler runs.

  6. 6

    Keyboard. Pointer events work only on objects; keys belong to the page. drei's <KeyboardControls map={[...]}> names keys ({ name: "left", keys: ["ArrowLeft", "KeyA"] }) and useKeyboardControls() returns [subscribe, get]. It listens on window unless you pass domElement, 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 in useFrame with no re-render. This is the keyboard twin of dragging, for people who cannot use a pointer (Topics 8.4 and 18.3).

  7. 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 in onPointerOver: document.body.style.cursor = "pointer", and restore it in onPointerOut.

Explain it without notes

01

A customer clicks the saddle and the frame behind it also reacts. Why, and how do you stop it?

02

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

01

Write a mesh that turns pink on hover and back to orange when the pointer leaves, using a ref and no state.

02

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