Skip to content

Events

Fresh

three.js objects that implement their own raycast method (meshes, lines, etc) can be interacted with by declaring events on them. R3F supports pointer events, clicks and wheel-scroll. Events contain the browser event as well as the three.js event data (object, point, distance, etc).

Additionally, there's a special onUpdate that is called every time the object gets fresh props.

Also notice the onPointerMissed on the canvas element, which fires on clicks that haven't hit any meshes.

jsx
<mesh
  onClick={(e) => console.log('click')}
  onContextMenu={(e) => console.log('context menu')}
  onDoubleClick={(e) => console.log('double click')}
  onWheel={(e) => console.log('wheel spins')}
  onPointerUp={(e) => console.log('up')}
  onPointerDown={(e) => console.log('down')}
  onPointerOver={(e) => console.log('over')}
  onPointerOut={(e) => console.log('out')}
  onPointerEnter={(e) => console.log('enter')}
  onPointerLeave={(e) => console.log('leave')}
  onPointerMove={(e) => console.log('move')}
  onPointerMissed={() => console.log('missed')}
  onUpdate={(self) => console.log('props have been updated')}
/>

Event Data

jsx
({
  ...DomEvent                   // All the original event data
  ...Intersection                 // All of Three's intersection data
  intersections: Intersection[]    // The first intersection of each intersected object
  object: Object3D              // The object that was actually hit
  eventObject: Object3D         // The object that registered the event
  unprojectedPoint: Vector3     // Camera-unprojected point
  ray: Ray                      // The ray that was used to strike the object
  camera: Camera                // The camera that was used in the raycaster
  sourceEvent: DomEvent         // A reference to the host event
  delta: number                 // Distance between mouse down and mouse up event in pixels
}) => ...

How the Event System Works

INFO

  • pointerenter and pointerleave events work exactly the same as pointerover and pointerout.
  • pointerenter and pointerleave semantics are not implemented.

INFO

Some events (such as pointerout) happen when there is no intersection between eventObject and the ray. When this happens, the event will contain intersection data from a previous event with this object.

Event Propagation (Bubbling)

Propagation works differently from the DOM because objects can occlude each other in 3D. The intersections array in the event includes all objects intersecting the ray, not just the nearest. Only the first intersection with each object is included.

The event is first delivered to the object nearest the camera, then bubbles up through its ancestors like in the DOM. After that, it is delivered to the next nearest object, and then its ancestors, and so on. This means objects are transparent to pointer events by default, even if the object handles the event.

event.stopPropagation() doesn't just stop this event from bubbling up, it also stops it from being delivered to farther objects (objects behind this one). If you want an object to block pointer events from objects behind it, it needs to have an event handler:

jsx
onPointerOver={e => {
  e.stopPropagation()
  // ...
}}

Pointer Capture

Because events go to all intersected objects, capturing the pointer also works differently. In the DOM, the capturing object replaces the hit test, but in R3F, the capturing object is added to the hit test result.

Note that you can access setPointerCapture and releasePointerCapture only via event.target.

jsx
onPointerDown={e => {
  // Only the mesh closest to the camera will be processed
  e.stopPropagation()
  // You may optionally capture the target
  e.target.setPointerCapture(e.pointerId)
}}
onPointerUp={e => {
  e.stopPropagation()
  // Optionally release capture
  e.target.releasePointerCapture(e.pointerId)
}}

Customizing Event Settings

For advanced usage, customize the event manager globally with the events prop on <Canvas/>:

tsx
import { Canvas, events } from '@react-three/fiber'

const eventManagerFactory = (state) => ({
  // Default configuration
  ...events(state),

  // Determines if the event layer is active
  enabled: true,

  // Event layer priority, higher prioritized layers come first
  priority: 1,

  // The filter can re-order or re-structure the intersections
  filter: (items, state) => items,

  // The compute defines how pointer events are translated into the raycaster
  compute: (event, state, previous) => {
    state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1)
    state.raycaster.setFromCamera(state.pointer, state.camera)
  },
})

function App() {
  return (
    <Canvas events={eventManagerFactory}>

Using a Different Target Element

Connect event handlers to another DOM element:

jsx
const events = useThree(state => state.events)
useEffect(() => {
  state.events.connect(domNode)

Or use the eventSource shortcut on the canvas:

jsx
function App() {
  const target = useRef()
  return (
    <div ref={target}>
      <Canvas eventSource={target.current}>

Using a Different Prefix

By default Fiber uses offsetX/offsetY to set up the raycaster. Change this with eventPrefix:

jsx
function App() {
  return (
    <Canvas eventPrefix="client">

Raycast Without User Interaction

By default Fiber only raycasts when the user is interacting with the canvas. To trigger a raycast when the camera moves:

jsx
const events = useThree(state => state.events)
useEffect(() => {
  // Will trigger a onPointerMove with the last-known pointer event
  state.events.update()

For complex cases:

jsx
function RaycastWhenCameraMoves() {
  const matrix = new THREE.Matrix4()
  useFrame((state) => {
    // Act only when the camera has moved
    if (!matrix.equals(state.camera.matrixWorld)) {
      state.events.update()
      matrix.copy(state.camera.matrixWorld)
    }
  })
}