r3f-interaction

React Three Fiber interaction - pointer events, controls, gestures, selection. Use when handling user input, implementing click detection, adding camera controls, or creating interactive 3D experiences.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "r3f-interaction" with this command: npx skills add enzed/r3f-skills/enzed-r3f-skills-r3f-interaction

React Three Fiber Interaction

Quick Start

import { Canvas } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei'

function InteractiveMesh() {
  return (
    <mesh
      onClick={(e) => console.log('Clicked!', e.point)}
      onPointerOver={(e) => console.log('Hover')}
      onPointerOut={(e) => console.log('Unhover')}
    >
      <boxGeometry />
      <meshStandardMaterial color="hotpink" />
    </mesh>
  )
}

export default function App() {
  return (
    <Canvas>
      <ambientLight />
      <InteractiveMesh />
      <OrbitControls />
    </Canvas>
  )
}

Pointer Events

R3F provides built-in pointer events on mesh elements.

Available Events

<mesh
  // Click events
  onClick={(e) => {}}           // Click (pointerdown + pointerup on same object)
  onDoubleClick={(e) => {}}     // Double click
  onContextMenu={(e) => {}}     // Right click

  // Pointer events
  onPointerDown={(e) => {}}     // Pointer pressed
  onPointerUp={(e) => {}}       // Pointer released
  onPointerMove={(e) => {}}     // Pointer moved while over object
  onPointerOver={(e) => {}}     // Pointer enters object
  onPointerOut={(e) => {}}      // Pointer leaves object
  onPointerEnter={(e) => {}}    // Pointer enters object (no bubbling)
  onPointerLeave={(e) => {}}    // Pointer leaves object (no bubbling)
  onPointerMissed={(e) => {}}   // Click that missed all objects

  // Wheel
  onWheel={(e) => {}}           // Mouse wheel

  // Touch
  onPointerCancel={(e) => {}}   // Touch cancelled
>
  <boxGeometry />
  <meshStandardMaterial />
</mesh>

Event Object

function InteractiveMesh() {
  const handleClick = (event) => {
    // Stop propagation to parent objects
    event.stopPropagation()

    // Event properties
    console.log({
      object: event.object,           // The mesh that was clicked
      point: event.point,             // World coordinates of intersection
      distance: event.distance,       // Distance from camera
      face: event.face,               // Intersected face
      faceIndex: event.faceIndex,     // Face index
      uv: event.uv,                   // UV coordinates at intersection
      normal: event.normal,           // Face normal
      camera: event.camera,           // Current camera
      ray: event.ray,                 // Ray used for intersection
      intersections: event.intersections, // All intersections
      nativeEvent: event.nativeEvent, // Original DOM event
      delta: event.delta,             // Click distance (useful for drag detection)
    })
  }

  return (
    <mesh onClick={handleClick}>
      <boxGeometry />
      <meshStandardMaterial />
    </mesh>
  )
}

Hover Effects

import { useState } from 'react'

function HoverableMesh() {
  const [hovered, setHovered] = useState(false)

  return (
    <mesh
      onPointerOver={(e) => {
        e.stopPropagation()
        setHovered(true)
        document.body.style.cursor = 'pointer'
      }}
      onPointerOut={(e) => {
        setHovered(false)
        document.body.style.cursor = 'default'
      }}
      scale={hovered ? 1.2 : 1}
    >
      <boxGeometry />
      <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
    </mesh>
  )
}

Selective Raycasting

// Disable raycasting for specific objects
<mesh raycast={() => null}>
  <boxGeometry />
  <meshStandardMaterial />
</mesh>

// Or use layers
<mesh
  layers={1}  // Only raycast against layer 1
  onClick={() => console.log('clicked')}
>
  <boxGeometry />
  <meshStandardMaterial />
</mesh>

Camera Controls

OrbitControls

import { OrbitControls } from '@react-three/drei'

function Scene() {
  return (
    <>
      <mesh>
        <boxGeometry />
        <meshStandardMaterial />
      </mesh>

      <OrbitControls
        makeDefault                    // Use as default controls
        enableDamping                  // Smooth movement
        dampingFactor={0.05}
        enableZoom={true}
        enablePan={true}
        enableRotate={true}
        autoRotate={false}
        autoRotateSpeed={2}
        minDistance={2}
        maxDistance={50}
        minPolarAngle={0}              // Top limit
        maxPolarAngle={Math.PI / 2}    // Horizon limit
        minAzimuthAngle={-Math.PI / 4} // Left limit
        maxAzimuthAngle={Math.PI / 4}  // Right limit
        target={[0, 1, 0]}             // Look-at point
      />
    </>
  )
}

OrbitControls with Ref

import { OrbitControls } from '@react-three/drei'
import { useRef, useEffect } from 'react'

function Scene() {
  const controlsRef = useRef()

  useEffect(() => {
    // Access controls methods
    if (controlsRef.current) {
      controlsRef.current.reset()
      controlsRef.current.target.set(0, 1, 0)
      controlsRef.current.update()
    }
  }, [])

  return <OrbitControls ref={controlsRef} />
}

MapControls

Top-down map-style controls.

import { MapControls } from '@react-three/drei'

<MapControls
  enableDamping
  dampingFactor={0.05}
  screenSpacePanning={false}  // Pan in world space
  maxPolarAngle={Math.PI / 2}
/>

FlyControls

Free-flying camera controls.

import { FlyControls } from '@react-three/drei'

<FlyControls
  movementSpeed={10}
  rollSpeed={Math.PI / 24}
  dragToLook
/>

FirstPersonControls

FPS-style controls.

import { FirstPersonControls } from '@react-three/drei'

<FirstPersonControls
  movementSpeed={10}
  lookSpeed={0.1}
  lookVertical
/>

PointerLockControls

Lock pointer for FPS games.

import { PointerLockControls } from '@react-three/drei'
import { useRef } from 'react'

function Scene() {
  const controlsRef = useRef()

  return (
    <>
      <PointerLockControls ref={controlsRef} />

      {/* Click to lock pointer */}
      <mesh onClick={() => controlsRef.current?.lock()}>
        <planeGeometry args={[10, 10]} />
        <meshBasicMaterial color="green" />
      </mesh>
    </>
  )
}

CameraControls

Advanced camera controls with smooth transitions.

import { CameraControls } from '@react-three/drei'
import { useRef } from 'react'

function Scene() {
  const controlsRef = useRef()

  const focusOnObject = async () => {
    // Smooth transition to target
    await controlsRef.current?.setLookAt(
      5, 3, 5,    // Camera position
      0, 0, 0,    // Look-at target
      true        // Enable transition
    )
  }

  return (
    <>
      <CameraControls ref={controlsRef} />

      <mesh onClick={focusOnObject}>
        <boxGeometry />
        <meshStandardMaterial color="red" />
      </mesh>
    </>
  )
}

TrackballControls

Unconstrained rotation controls.

import { TrackballControls } from '@react-three/drei'

<TrackballControls
  rotateSpeed={2.0}
  zoomSpeed={1.2}
  panSpeed={0.8}
  staticMoving={true}
/>

ArcballControls

Arc-based rotation controls.

import { ArcballControls } from '@react-three/drei'

<ArcballControls
  enableAnimations
  dampingFactor={25}
/>

Transform Controls

Gizmo for moving/rotating/scaling objects.

import { TransformControls, OrbitControls } from '@react-three/drei'
import { useRef, useState } from 'react'

function Scene() {
  const meshRef = useRef()
  const [mode, setMode] = useState('translate')
  const orbitRef = useRef()

  return (
    <>
      <OrbitControls ref={orbitRef} makeDefault />

      <TransformControls
        object={meshRef}
        mode={mode}  // 'translate' | 'rotate' | 'scale'
        space="local"  // 'local' | 'world'
        onMouseDown={() => {
          // Disable orbit while transforming
          if (orbitRef.current) orbitRef.current.enabled = false
        }}
        onMouseUp={() => {
          if (orbitRef.current) orbitRef.current.enabled = true
        }}
      />

      <mesh ref={meshRef}>
        <boxGeometry />
        <meshStandardMaterial color="orange" />
      </mesh>

      {/* Mode switching buttons in HTML */}
      <div className="controls">
        <button onClick={() => setMode('translate')}>Move</button>
        <button onClick={() => setMode('rotate')}>Rotate</button>
        <button onClick={() => setMode('scale')}>Scale</button>
      </div>
    </>
  )
}

PivotControls

Alternative transform gizmo with pivot point.

import { PivotControls } from '@react-three/drei'

function Scene() {
  return (
    <PivotControls
      anchor={[0, 0, 0]}         // Anchor point
      depthTest={false}          // Always visible
      lineWidth={2}              // Axis line width
      axisColors={['red', 'green', 'blue']}
      scale={1}                  // Gizmo scale
      fixed={false}              // Fixed screen size
    >
      <mesh>
        <boxGeometry />
        <meshStandardMaterial color="orange" />
      </mesh>
    </PivotControls>
  )
}

Drag Controls

useDrag from @use-gesture/react

npm install @use-gesture/react
import { useDrag } from '@use-gesture/react'
import { useSpring, animated } from '@react-spring/three'
import { useThree } from '@react-three/fiber'

function DraggableMesh() {
  const { size, viewport } = useThree()
  const aspect = size.width / viewport.width

  const [spring, api] = useSpring(() => ({
    position: [0, 0, 0],
    config: { mass: 1, tension: 280, friction: 60 }
  }))

  const bind = useDrag(({ movement: [mx, my], down }) => {
    api.start({
      position: down ? [mx / aspect, -my / aspect, 0] : [0, 0, 0]
    })
  })

  return (
    <animated.mesh {...bind()} position={spring.position}>
      <boxGeometry />
      <meshStandardMaterial color="hotpink" />
    </animated.mesh>
  )
}

DragControls (Drei)

import { DragControls, OrbitControls } from '@react-three/drei'
import { useRef } from 'react'

function Scene() {
  const meshRef = useRef()
  const orbitRef = useRef()

  return (
    <>
      <OrbitControls ref={orbitRef} makeDefault />

      <DragControls
        onDragStart={() => {
          if (orbitRef.current) orbitRef.current.enabled = false
        }}
        onDragEnd={() => {
          if (orbitRef.current) orbitRef.current.enabled = true
        }}
      >
        <mesh ref={meshRef}>
          <boxGeometry />
          <meshStandardMaterial color="orange" />
        </mesh>
      </DragControls>
    </>
  )
}

Keyboard Controls

KeyboardControls (Drei)

import { KeyboardControls, useKeyboardControls } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
import { useRef } from 'react'

// Define key mappings
const keyMap = [
  { name: 'forward', keys: ['ArrowUp', 'KeyW'] },
  { name: 'backward', keys: ['ArrowDown', 'KeyS'] },
  { name: 'left', keys: ['ArrowLeft', 'KeyA'] },
  { name: 'right', keys: ['ArrowRight', 'KeyD'] },
  { name: 'jump', keys: ['Space'] },
  { name: 'sprint', keys: ['ShiftLeft'] },
]

function Player() {
  const meshRef = useRef()
  const [, getKeys] = useKeyboardControls()

  useFrame((state, delta) => {
    const { forward, backward, left, right, jump, sprint } = getKeys()

    const speed = sprint ? 10 : 5

    if (forward) meshRef.current.position.z -= speed * delta
    if (backward) meshRef.current.position.z += speed * delta
    if (left) meshRef.current.position.x -= speed * delta
    if (right) meshRef.current.position.x += speed * delta
    if (jump) meshRef.current.position.y += speed * delta
  })

  return (
    <mesh ref={meshRef}>
      <boxGeometry />
      <meshStandardMaterial color="blue" />
    </mesh>
  )
}

export default function App() {
  return (
    <KeyboardControls map={keyMap}>
      <Canvas>
        <ambientLight />
        <Player />
      </Canvas>
    </KeyboardControls>
  )
}

Subscribe to Key Changes

import { useKeyboardControls } from '@react-three/drei'
import { useEffect } from 'react'

function KeyListener() {
  const jumpPressed = useKeyboardControls((state) => state.jump)

  useEffect(() => {
    if (jumpPressed) {
      console.log('Jump!')
    }
  }, [jumpPressed])

  return null
}

Selection System

Click to Select

import { useState } from 'react'

function SelectableScene() {
  const [selected, setSelected] = useState(null)

  return (
    <>
      {[[-2, 0, 0], [0, 0, 0], [2, 0, 0]].map((position, i) => (
        <mesh
          key={i}
          position={position}
          onClick={(e) => {
            e.stopPropagation()
            setSelected(i)
          }}
        >
          <boxGeometry />
          <meshStandardMaterial
            color={selected === i ? 'hotpink' : 'orange'}
            emissive={selected === i ? 'hotpink' : 'black'}
            emissiveIntensity={0.3}
          />
        </mesh>
      ))}

      {/* Click on empty space to deselect */}
      <mesh
        position={[0, -1, 0]}
        rotation={[-Math.PI / 2, 0, 0]}
        onClick={() => setSelected(null)}
      >
        <planeGeometry args={[20, 20]} />
        <meshStandardMaterial color="gray" />
      </mesh>
    </>
  )
}

Multi-Select with Outline

import { useState } from 'react'
import { EffectComposer, Outline, Selection, Select } from '@react-three/postprocessing'

function MultiSelectScene() {
  const [selected, setSelected] = useState(new Set())

  const toggleSelect = (id, event) => {
    event.stopPropagation()
    setSelected((prev) => {
      const next = new Set(prev)
      if (event.shiftKey) {
        // Multi-select with shift
        if (next.has(id)) {
          next.delete(id)
        } else {
          next.add(id)
        }
      } else {
        // Single select
        next.clear()
        next.add(id)
      }
      return next
    })
  }

  return (
    <Selection>
      <EffectComposer autoClear={false}>
        <Outline
          blur
          visibleEdgeColor={0xffffff}
          edgeStrength={10}
        />
      </EffectComposer>

      {[0, 1, 2, 3, 4].map((id) => (
        <Select key={id} enabled={selected.has(id)}>
          <mesh
            position={[(id - 2) * 2, 0, 0]}
            onClick={(e) => toggleSelect(id, e)}
          >
            <boxGeometry />
            <meshStandardMaterial color="orange" />
          </mesh>
        </Select>
      ))}
    </Selection>
  )
}

Screen-Space to World-Space

Get World Position from Click

import { useThree } from '@react-three/fiber'
import * as THREE from 'three'

function ClickToPlace() {
  const { camera, raycaster, pointer } = useThree()
  const planeRef = useRef()

  const handleClick = (event) => {
    // Create intersection plane
    const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
    const intersection = new THREE.Vector3()

    // Cast ray from pointer
    raycaster.setFromCamera(pointer, camera)
    raycaster.ray.intersectPlane(plane, intersection)

    console.log('World position:', intersection)
  }

  return (
    <mesh
      ref={planeRef}
      rotation={[-Math.PI / 2, 0, 0]}
      onClick={handleClick}
    >
      <planeGeometry args={[100, 100]} />
      <meshBasicMaterial visible={false} />
    </mesh>
  )
}

World Position to Screen Position

import { useThree, useFrame } from '@react-three/fiber'
import { Html } from '@react-three/drei'
import * as THREE from 'three'

function WorldToScreen({ target }) {
  const { camera, size } = useThree()

  const getScreenPosition = (worldPos) => {
    const vector = worldPos.clone()
    vector.project(camera)

    return {
      x: (vector.x * 0.5 + 0.5) * size.width,
      y: (1 - (vector.y * 0.5 + 0.5)) * size.height
    }
  }

  // Or use Html component which handles this automatically
  return (
    <Html position={target}>
      <div className="label">Label</div>
    </Html>
  )
}

Gesture Recognition

usePinch and useWheel

import { usePinch, useWheel } from '@use-gesture/react'
import { useSpring, animated } from '@react-spring/three'

function ZoomableMesh() {
  const [spring, api] = useSpring(() => ({
    scale: 1,
    config: { mass: 1, tension: 200, friction: 30 }
  }))

  usePinch(
    ({ offset: [s] }) => {
      api.start({ scale: s })
    },
    { target: window }
  )

  useWheel(
    ({ delta: [, dy] }) => {
      api.start({ scale: spring.scale.get() - dy * 0.001 })
    },
    { target: window }
  )

  return (
    <animated.mesh scale={spring.scale}>
      <boxGeometry />
      <meshStandardMaterial color="cyan" />
    </animated.mesh>
  )
}

Scroll Controls

import { Canvas } from '@react-three/fiber'
import { ScrollControls, Scroll, useScroll } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
import { useRef } from 'react'

function AnimatedOnScroll() {
  const meshRef = useRef()
  const scroll = useScroll()

  useFrame(() => {
    const offset = scroll.offset // 0 to 1
    meshRef.current.rotation.y = offset * Math.PI * 2
    meshRef.current.position.y = offset * 5
  })

  return (
    <mesh ref={meshRef}>
      <boxGeometry />
      <meshStandardMaterial color="orange" />
    </mesh>
  )
}

export default function App() {
  return (
    <Canvas>
      <ScrollControls pages={3} damping={0.25}>
        <Scroll>
          <AnimatedOnScroll />
        </Scroll>

        {/* HTML content that scrolls */}
        <Scroll html>
          <h1 style={{ position: 'absolute', top: '10vh' }}>Page 1</h1>
          <h1 style={{ position: 'absolute', top: '110vh' }}>Page 2</h1>
          <h1 style={{ position: 'absolute', top: '210vh' }}>Page 3</h1>
        </Scroll>
      </ScrollControls>
    </Canvas>
  )
}

Presentation Controls

For product showcases with limited rotation.

import { PresentationControls } from '@react-three/drei'

function ProductShowcase() {
  return (
    <PresentationControls
      global                 // Apply to whole scene
      snap                   // Snap back when released
      speed={1}              // Rotation speed
      zoom={1}               // Zoom speed
      rotation={[0, 0, 0]}   // Initial rotation
      polar={[-Math.PI / 4, Math.PI / 4]}    // Vertical limits
      azimuth={[-Math.PI / 4, Math.PI / 4]}  // Horizontal limits
      config={{ mass: 1, tension: 170, friction: 26 }}
    >
      <mesh>
        <boxGeometry />
        <meshStandardMaterial color="gold" />
      </mesh>
    </PresentationControls>
  )
}

Performance Tips

  1. Stop propagation: Prevent unnecessary raycasts
  2. Use layers: Filter raycast targets
  3. Simpler collision meshes: Use invisible simple geometry
  4. Throttle events: Limit onPointerMove frequency
  5. Disable controls when not needed: enabled={false}
// Use simpler geometry for raycasting
function OptimizedInteraction() {
  return (
    <group>
      {/* Complex visible mesh */}
      <mesh raycast={() => null}>
        <torusKnotGeometry args={[1, 0.4, 100, 16]} />
        <meshStandardMaterial color="purple" />
      </mesh>

      {/* Simple invisible collision mesh */}
      <mesh onClick={() => console.log('clicked')}>
        <sphereGeometry args={[1.5]} />
        <meshBasicMaterial visible={false} />
      </mesh>
    </group>
  )
}

// Throttle pointer move events
import { useMemo, useCallback } from 'react'
import throttle from 'lodash/throttle'

function ThrottledHover() {
  const handleMove = useMemo(
    () => throttle((e) => {
      console.log('Move', e.point)
    }, 100),
    []
  )

  return (
    <mesh onPointerMove={handleMove}>
      <boxGeometry />
      <meshStandardMaterial />
    </mesh>
  )
}

See Also

  • r3f-fundamentals - Canvas and scene setup
  • r3f-animation - Animating interactions
  • r3f-postprocessing - Visual feedback effects (outline, selection)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

r3f-fundamentals

No summary provided by upstream source.

Repository SourceNeeds Review
271-enzed
General

r3f-geometry

No summary provided by upstream source.

Repository SourceNeeds Review
248-enzed
General

r3f-shaders

No summary provided by upstream source.

Repository SourceNeeds Review
246-enzed
General

r3f-physics

No summary provided by upstream source.

Repository SourceNeeds Review
237-enzed