React Three Fiber Interaction Quick Start import { Canvas } from '@react-three/fiber' import { OrbitControls } from '@react-three/drei'
function InteractiveMesh() {
return (
export default function App() { return ( ) }
Pointer Events
R3F provides built-in pointer events on mesh elements.
Available Events
// 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
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 (
Hover Effects import { useState } from 'react'
function HoverableMesh() { const [hovered, setHovered] = useState(false)
return (
Selective Raycasting
// Disable raycasting for specific objects
// Or use layers
Camera Controls OrbitControls import { OrbitControls } from '@react-three/drei'
function Scene() {
return (
<>
<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
MapControls
Top-down map-style controls.
import { MapControls } from '@react-three/drei'
FlyControls
Free-flying camera controls.
import { FlyControls } from '@react-three/drei'
FirstPersonControls
FPS-style controls.
import { FirstPersonControls } from '@react-three/drei'
PointerLockControls
Lock pointer for FPS games.
import { PointerLockControls } from '@react-three/drei' import { useRef } from 'react'
function Scene() { const controlsRef = useRef()
return (
<>
{/* 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 (
<>
<mesh onClick={focusOnObject}>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
</>
) }
TrackballControls
Unconstrained rotation controls.
import { TrackballControls } from '@react-three/drei'
ArcballControls
Arc-based rotation controls.
import { ArcballControls } from '@react-three/drei'
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 (
<>
<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 (
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 (
DragControls (Drei) import { DragControls, OrbitControls } from '@react-three/drei' import { useRef } from 'react'
function Scene() { const meshRef = useRef() const orbitRef = useRef()
return (
<>
<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 (
export default function App() {
return (
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) => (
{/* 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 (
{[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 (
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 (
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 (
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 (
export default function App() { return (
{/* 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 (
Performance Tips
Stop propagation: Prevent unnecessary raycasts
Use layers: Filter raycast targets
Simpler collision meshes: Use invisible simple geometry
Throttle events: Limit onPointerMove frequency
Disable controls when not needed: enabled={false}
// Use simpler geometry for raycasting
function OptimizedInteraction() {
return (
{/* 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 (
See Also r3f-fundamentals - Canvas and scene setup r3f-animation - Animating interactions r3f-postprocessing - Visual feedback effects (outline, selection)