
R3f Interaction
- 2.2k installs
- 105 repo stars
- Updated January 20, 2026
- enzed/r3f-skills
r3f-interaction is a React Three Fiber skill for pointer events, controls, gestures, and 3D selection.
About
The r3f-interaction skill documents React Three Fiber user input patterns including pointer events, camera controls, gestures, and object selection for interactive 3D experiences. Quick start shows mesh onClick, onPointerOver, and onPointerOut handlers inside a Canvas with OrbitControls from drei. Available events span click, double-click, context menu, pointer down/up/move/over/out/enter/leave, wheel, and pointerMissed for background clicks. Event objects expose stopPropagation, intersections, point, object, and camera data for precise hit testing. Control sections cover OrbitControls, MapControls, FlyControls, and custom camera rigs with damping and constraint options. Selection patterns explain raycasting, multi-select, hover highlighting via material swaps, and draggable objects using pivot or transform controls. Performance guidance notes event handler stability, pointer capture, and limiting raycast targets. Examples progress from basic clickable meshes through gesture-heavy scenes suitable for product viewers, games, and data visualizations built with R3F.
- Documents full R3F pointer, wheel, and pointerMissed event surface.
- Shows OrbitControls and additional camera control integrations.
- Covers raycast selection, hover states, and draggable object patterns.
- Explains event object fields for intersections and propagation control.
- Targets interactive 3D apps using React Three Fiber and drei helpers.
R3f Interaction by the numbers
- 2,217 all-time installs (skills.sh)
- +41 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #210 of 2,277 Frontend Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
r3f-interaction capabilities & compatibility
- Capabilities
- pointer, wheel, and missed click event coverage · orbit and advanced camera control setup · raycast based selection and hover highlighting · draggable and transform control patterns · event object intersection and propagation handli
- Use cases
- frontend · ui design
What r3f-interaction says it does
R3F provides built-in pointer events on mesh elements
npx skills add https://github.com/enzed/r3f-skills --skill r3f-interactionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.2k |
|---|---|
| repo stars | ★ 105 |
| Security audit | 3 / 3 scanners passed |
| Last updated | January 20, 2026 |
| Repository | enzed/r3f-skills ↗ |
How do I handle clicks, hovers, and camera controls in a React Three Fiber scene?
Implement React Three Fiber pointer events, camera controls, gestures, and interactive 3D selection patterns.
Who is it for?
R3F projects needing click detection, orbit cameras, or draggable 3D objects.
Skip if: Skip for non-React Three.js vanilla setups without the R3F component model.
When should I use this skill?
User implements 3D clicks, pointer events, OrbitControls, or interactive R3F experiences.
What you get
Interactive meshes with correct pointer handlers, controls, and selection behavior.
- Pointer event handlers on meshes
- Configured camera controls
- Drag, keyboard, or selection interactions
By the numbers
- Documents 8 @react-three/drei camera control components
- Lists 10 mesh pointer event handlers including onPointerMissed and onWheel
- Includes KeyboardControls keyMap example with 6 mapped actions
Files
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/reactimport { 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 setupr3f-animation- Animating interactionsr3f-postprocessing- Visual feedback effects (outline, selection)
Related skills
How it compares
Use r3f-interaction for input and controls; use r3f-fundamentals for Canvas setup and r3f-postprocessing for visual selection effects beyond basic outlines.
FAQ
How do I stop clicks bubbling to parent meshes?
Call event.stopPropagation() inside the mesh handler to limit propagation.
What event fires when clicking empty space?
Use onPointerMissed on the Canvas or parent to handle background clicks.
Which helper adds orbit camera controls?
Import OrbitControls from @react-three/drei inside the Canvas tree.
Is R3f Interaction safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.