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.
How do I install this agent skill?
npx skills add https://github.com/enzed/r3f-skills --skill r3f-interactionIs this agent skill safe to install?
- Gen Agent Trust Hubpass
No files were provided for analysis. This skill contains no code, instructions, or metadata, posing no security risk.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
- Runlayerpass
1 file scanned · No issues
- ZeroLeakspass
Score: 93/100 · 2 sections analyzed
What does this agent skill do?
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
- 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 (
<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)
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/enzed/r3f-skills/r3f-interaction">View r3f-interaction on skillZs</a>