From enzed-r3f-skills
Covers React Three Fiber fundamentals: Canvas setup, useFrame/useThree hooks, JSX elements, events, refs. Use for R3F scene creation, components, render loops, Three.js objects in React.
npx claudepluginhub joshuarweaver/cascade-code-languages-misc-1 --plugin enzed-r3f-skillsThis skill uses the workspace's default tool permissions.
```tsx
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
import { Canvas } from '@react-three/fiber'
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
function RotatingBox() {
const meshRef = useRef()
useFrame((state, delta) => {
meshRef.current.rotation.x += delta
meshRef.current.rotation.y += delta * 0.5
})
return (
<mesh ref={meshRef}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="hotpink" />
</mesh>
)
}
export default function App() {
return (
<Canvas camera={{ position: [0, 0, 5], fov: 75 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} />
<RotatingBox />
</Canvas>
)
}
The root component that creates the WebGL context, scene, camera, and renderer.
import { Canvas } from '@react-three/fiber'
function App() {
return (
<Canvas
// Camera configuration
camera={{
position: [0, 5, 10],
fov: 75,
near: 0.1,
far: 1000,
}}
// Or use orthographic
orthographic
camera={{ zoom: 50, position: [0, 0, 100] }}
// Renderer settings
gl={{
antialias: true,
alpha: true,
powerPreference: 'high-performance',
preserveDrawingBuffer: true, // For screenshots
}}
dpr={[1, 2]} // Pixel ratio min/max
// Shadows
shadows // or shadows="soft" | "basic" | "percentage"
// Color management
flat // Disable automatic sRGB color management
// Frame loop control
frameloop="demand" // 'always' | 'demand' | 'never'
// Event handling
eventSource={document.getElementById('root')}
eventPrefix="client" // 'offset' | 'client' | 'page' | 'layer' | 'screen'
// Callbacks
onCreated={(state) => {
console.log('Canvas ready:', state.gl, state.scene, state.camera)
}}
onPointerMissed={() => console.log('Clicked background')}
// Styling
style={{ width: '100%', height: '100vh' }}
>
<Scene />
</Canvas>
)
}
R3F sets sensible defaults:
Subscribe to the render loop. Called every frame (typically 60fps).
import { useFrame } from '@react-three/fiber'
import { useRef } from 'react'
function AnimatedMesh() {
const meshRef = useRef()
useFrame((state, delta, xrFrame) => {
// state: Full R3F state (see useThree)
// delta: Time since last frame in seconds
// xrFrame: XR frame if in VR/AR mode
// Animate rotation
meshRef.current.rotation.y += delta
// Access clock
const elapsed = state.clock.elapsedTime
meshRef.current.position.y = Math.sin(elapsed) * 2
// Access pointer position (-1 to 1)
const { x, y } = state.pointer
meshRef.current.rotation.x = y * 0.5
meshRef.current.rotation.z = x * 0.5
})
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
)
}
Control render order with priority (higher = later).
// Default priority is 0
useFrame((state, delta) => {
// Runs first
}, -1)
useFrame((state, delta) => {
// Runs after priority -1
}, 0)
// Manual rendering with positive priority
useFrame((state, delta) => {
// Take over rendering
state.gl.render(state.scene, state.camera)
}, 1)
function ConditionalAnimation({ active }) {
useFrame((state, delta) => {
if (!active) return // Skip when inactive
meshRef.current.rotation.y += delta
})
}
Access the R3F state store.
import { useThree } from '@react-three/fiber'
function CameraInfo() {
// Get full state (triggers re-render on any change)
const state = useThree()
// Selective subscription (recommended)
const camera = useThree((state) => state.camera)
const gl = useThree((state) => state.gl)
const scene = useThree((state) => state.scene)
const size = useThree((state) => state.size)
// Available state properties:
// gl: WebGLRenderer
// scene: Scene
// camera: Camera
// raycaster: Raycaster
// pointer: Vector2 (normalized -1 to 1)
// mouse: Vector2 (deprecated, use pointer)
// clock: Clock
// size: { width, height, top, left }
// viewport: { width, height, factor, distance, aspect }
// performance: { current, min, max, debounce, regress }
// events: Event handlers
// set: State setter
// get: State getter
// invalidate: Trigger re-render (for frameloop="demand")
// advance: Advance one frame (for frameloop="never")
return null
}
// Responsive to viewport
function ResponsiveObject() {
const viewport = useThree((state) => state.viewport)
return (
<mesh scale={[viewport.width / 4, viewport.height / 4, 1]}>
<planeGeometry />
<meshBasicMaterial color="blue" />
</mesh>
)
}
// Manual render trigger
function TriggerRender() {
const invalidate = useThree((state) => state.invalidate)
const handleClick = () => {
// Trigger render when using frameloop="demand"
invalidate()
}
}
// Update camera
function CameraController() {
const camera = useThree((state) => state.camera)
const set = useThree((state) => state.set)
useEffect(() => {
camera.position.set(10, 10, 10)
camera.lookAt(0, 0, 0)
}, [camera])
}
All Three.js objects are available as JSX elements (camelCase).
// Basic mesh structure
<mesh
position={[0, 0, 0]} // x, y, z
rotation={[0, Math.PI, 0]} // Euler angles in radians
scale={[1, 2, 1]} // x, y, z or single number
visible={true}
castShadow
receiveShadow
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="red" />
</mesh>
// With ref
const meshRef = useRef()
<mesh ref={meshRef} />
// meshRef.current is the THREE.Mesh
Constructor arguments via args prop:
// BoxGeometry(width, height, depth, widthSegments, heightSegments, depthSegments)
<boxGeometry args={[1, 1, 1, 1, 1, 1]} />
// SphereGeometry(radius, widthSegments, heightSegments)
<sphereGeometry args={[1, 32, 32]} />
// PlaneGeometry(width, height, widthSegments, heightSegments)
<planeGeometry args={[10, 10]} />
// CylinderGeometry(radiusTop, radiusBottom, height, radialSegments)
<cylinderGeometry args={[1, 1, 2, 32]} />
<group position={[5, 0, 0]} rotation={[0, Math.PI / 4, 0]}>
<mesh position={[-1, 0, 0]}>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
<mesh position={[1, 0, 0]}>
<boxGeometry />
<meshStandardMaterial color="blue" />
</mesh>
</group>
Use dashes for nested properties:
<mesh
position-x={5}
rotation-y={Math.PI}
scale-z={2}
>
<meshStandardMaterial
color="red"
metalness={0.8}
roughness={0.2}
/>
</mesh>
// Shadow camera properties
<directionalLight
castShadow
shadow-mapSize={[2048, 2048]}
shadow-camera-left={-10}
shadow-camera-right={10}
shadow-camera-top={10}
shadow-camera-bottom={-10}
/>
Control how children attach to parents:
<mesh>
<boxGeometry />
{/* Default: attaches as 'material' */}
<meshStandardMaterial />
</mesh>
{/* Explicit attach */}
<mesh>
<boxGeometry attach="geometry" />
<meshStandardMaterial attach="material" />
</mesh>
{/* Array attachment */}
<mesh>
<boxGeometry />
<meshStandardMaterial attach="material-0" color="red" />
<meshStandardMaterial attach="material-1" color="blue" />
</mesh>
{/* Custom attachment with function */}
<someObject>
<texture
attach={(parent, self) => {
parent.map = self
return () => { parent.map = null } // Cleanup
}}
/>
</someObject>
R3F provides React-style events on 3D objects.
function InteractiveBox() {
const [hovered, setHovered] = useState(false)
const [clicked, setClicked] = useState(false)
return (
<mesh
onClick={(e) => {
e.stopPropagation() // Prevent bubbling
setClicked(!clicked)
// Event properties:
console.log(e.object) // THREE.Mesh
console.log(e.point) // Vector3 - intersection point
console.log(e.distance) // Distance from camera
console.log(e.face) // Intersected face
console.log(e.faceIndex) // Face index
console.log(e.uv) // UV coordinates
console.log(e.normal) // Face normal
console.log(e.pointer) // Normalized pointer coords
console.log(e.ray) // Raycaster ray
console.log(e.camera) // Camera
console.log(e.delta) // Distance moved (drag events)
}}
onContextMenu={(e) => console.log('Right click')}
onDoubleClick={(e) => console.log('Double click')}
onPointerOver={(e) => {
e.stopPropagation()
setHovered(true)
document.body.style.cursor = 'pointer'
}}
onPointerOut={(e) => {
setHovered(false)
document.body.style.cursor = 'default'
}}
onPointerDown={(e) => console.log('Pointer down')}
onPointerUp={(e) => console.log('Pointer up')}
onPointerMove={(e) => console.log('Moving over mesh')}
onWheel={(e) => console.log('Wheel:', e.deltaY)}
scale={hovered ? 1.2 : 1}
>
<boxGeometry />
<meshStandardMaterial color={clicked ? 'hotpink' : 'orange'} />
</mesh>
)
}
Events bubble up through the scene graph:
<group onClick={(e) => console.log('Group clicked')}>
<mesh onClick={(e) => {
e.stopPropagation() // Stop bubbling to group
console.log('Mesh clicked')
}}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
</group>
Use existing Three.js objects directly:
import * as THREE from 'three'
// Existing object
const geometry = new THREE.BoxGeometry()
const material = new THREE.MeshStandardMaterial({ color: 'red' })
const mesh = new THREE.Mesh(geometry, material)
function Scene() {
return <primitive object={mesh} position={[0, 1, 0]} />
}
// Common with loaded models
function Model({ gltf }) {
return <primitive object={gltf.scene} />
}
Register custom Three.js classes for JSX use:
import { extend } from '@react-three/fiber'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
// Extend once (usually at module level)
extend({ OrbitControls })
// Now use as JSX
function Scene() {
const { camera, gl } = useThree()
return <orbitControls args={[camera, gl.domElement]} />
}
// TypeScript declaration
declare global {
namespace JSX {
interface IntrinsicElements {
orbitControls: ReactThreeFiber.Object3DNode<OrbitControls, typeof OrbitControls>
}
}
}
import { useRef, useEffect } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
function MeshWithRef() {
const meshRef = useRef<THREE.Mesh>(null)
const materialRef = useRef<THREE.MeshStandardMaterial>(null)
useEffect(() => {
if (meshRef.current) {
// Direct Three.js access
meshRef.current.geometry.computeBoundingBox()
console.log(meshRef.current.geometry.boundingBox)
}
}, [])
useFrame(() => {
if (materialRef.current) {
materialRef.current.color.setHSL(Math.random(), 1, 0.5)
}
})
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial ref={materialRef} />
</mesh>
)
}
// BAD: Creates new object every render
<mesh position={[x, y, z]} />
// GOOD: Mutate existing position
const meshRef = useRef()
useFrame(() => {
meshRef.current.position.x = x
})
<mesh ref={meshRef} />
// GOOD: Use useMemo for static values
const position = useMemo(() => [x, y, z], [x, y, z])
<mesh position={position} />
// Isolate animated components to prevent parent re-renders
function Scene() {
return (
<>
<StaticEnvironment />
<AnimatedObject /> {/* Only this re-renders on animation */}
</>
)
}
function AnimatedObject() {
const ref = useRef()
useFrame((_, delta) => {
ref.current.rotation.y += delta
})
return <mesh ref={ref}><boxGeometry /></mesh>
}
R3F auto-disposes geometries, materials, and textures. Override with:
<mesh dispose={null}> {/* Prevent auto-dispose */}
<boxGeometry />
<meshStandardMaterial />
</mesh>
// styles.css
html, body, #root {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
// App.tsx
<Canvas style={{ width: '100%', height: '100%' }}>
function ResponsiveScene() {
const { viewport } = useThree()
return (
<mesh scale={Math.min(viewport.width, viewport.height) / 5}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
)
}
import { forwardRef } from 'react'
const CustomMesh = forwardRef((props, ref) => {
return (
<mesh ref={ref} {...props}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
)
})
// Usage
const meshRef = useRef()
<CustomMesh ref={meshRef} position={[0, 1, 0]} />
Leva provides a GUI for tweaking parameters in real-time during development.
npm install leva
import { useControls } from 'leva'
function DebugMesh() {
const { position, color, scale, visible } = useControls({
position: { value: [0, 0, 0], step: 0.1 },
color: '#ff0000',
scale: { value: 1, min: 0.1, max: 5, step: 0.1 },
visible: true,
})
return (
<mesh position={position} scale={scale} visible={visible}>
<boxGeometry />
<meshStandardMaterial color={color} />
</mesh>
)
}
import { useControls, folder } from 'leva'
function DebugScene() {
const { lightIntensity, lightColor, shadowMapSize } = useControls({
Lighting: folder({
lightIntensity: { value: 1, min: 0, max: 5 },
lightColor: '#ffffff',
shadowMapSize: { value: 1024, options: [512, 1024, 2048, 4096] },
}),
Camera: folder({
fov: { value: 75, min: 30, max: 120 },
near: { value: 0.1, min: 0.01, max: 1 },
}),
})
return (
<directionalLight
intensity={lightIntensity}
color={lightColor}
shadow-mapSize={[shadowMapSize, shadowMapSize]}
/>
)
}
import { useControls, button } from 'leva'
function DebugActions() {
const meshRef = useRef()
useControls({
'Reset Position': button(() => {
meshRef.current.position.set(0, 0, 0)
}),
'Random Color': button(() => {
meshRef.current.material.color.setHex(Math.random() * 0xffffff)
}),
'Log State': button(() => {
console.log(meshRef.current.position)
}),
})
return <mesh ref={meshRef}>...</mesh>
}
import { Leva } from 'leva'
function App() {
return (
<>
{/* Hide Leva panel in production */}
<Leva hidden={process.env.NODE_ENV === 'production'} />
<Canvas>
<Scene />
</Canvas>
</>
)
}
import { useControls, monitor } from 'leva'
import { useFrame } from '@react-three/fiber'
function PerformanceMonitor() {
const [fps, setFps] = useState(0)
useControls({
FPS: monitor(() => fps, { graph: true, interval: 100 }),
})
useFrame((state) => {
// Update FPS display
setFps(Math.round(1 / state.clock.getDelta()))
})
return null
}
function AnimatedDebugMesh() {
const meshRef = useRef()
const { speed, amplitude, enabled } = useControls('Animation', {
enabled: true,
speed: { value: 1, min: 0, max: 5 },
amplitude: { value: 1, min: 0, max: 3 },
})
useFrame(({ clock }) => {
if (!enabled) return
meshRef.current.position.y = Math.sin(clock.elapsedTime * speed) * amplitude
})
return (
<mesh ref={meshRef}>
<sphereGeometry />
<meshStandardMaterial color="cyan" />
</mesh>
)
}
r3f-geometry - Geometry creationr3f-materials - Material configurationr3f-lighting - Lights and shadowsr3f-interaction - Controls and user input