Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
enzed avatar

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)
At a glance

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
From the docs

What r3f-interaction says it does

R3F provides built-in pointer events on mesh elements
SKILL.md
npx skills add https://github.com/enzed/r3f-skills --skill r3f-interaction

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs2.2k
repo stars105
Security audit3 / 3 scanners passed
Last updatedJanuary 20, 2026
Repositoryenzed/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

SKILL.mdMarkdownGitHub ↗

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)

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.

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.