r3f-interaction

Pointer events, camera controls, gestures, and selection for interactive 3D scenes. Supports 10+ pointer events (click, hover, drag, wheel) with rich intersection data including world position, UV coordinates, and face normals Includes 8 camera control presets: OrbitControls, MapControls, FlyControls, FirstPersonControls, PointerLockControls, CameraControls, TrackballControls, and ArcballControls Provides transform gizmos (TransformControls, PivotControls) and drag systems for object manipulation, plus KeyboardControls for WASD-style input Covers selection systems with single/multi-select, outline effects, scroll-driven animation, and screen-to-world coordinate conversion

INSTALLATION
npx skills add https://github.com/enzed/r3f-skills --skill r3f-interaction
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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')}

>

)

}

export default function App() {

return (

)

}

## 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>

)

}

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card