Building your First Timeline.
Step-by-step - create a React scene and orchestrate camera behavior with a timeline.
This walks you through building the intro example from scratch.
Final result
import { Canvas, useThree } from '@react-three/fiber' import { Text, Environment } from '@react-three/drei' import { useRef } from 'react' import { Mesh } from 'three' import { useTimeline, action, lookAt, spring, springPresets } from '@react-three/timeline' function Scene() { const camera = useThree((s) => s.camera) const red = useRef<Mesh>(null) const blue = useRef<Mesh>(null) useTimeline(async function* () { while (true) { yield* action({ update: lookAt(camera, red.current!, spring(springPresets.stiff)) }) yield* action({ update: lookAt(camera, blue.current!, spring(springPresets.stiff)) }) } }, []) return ( <> <Text color="black" position-y={1} scale={0.3}>Your first timeline</Text> <mesh ref={red} position-x={-2} position-y={-1} rotation-y={(-30 / 180) * Math.PI} scale={[0.2, 0.2, 0.4]}> <sphereGeometry /> <meshPhysicalMaterial emissive="red" emissiveIntensity={1.5} color="red" /> </mesh> <mesh ref={blue} position-x={2} position-y={-1} rotation-y={(20 / 180) * Math.PI} scale={[0.2, 0.2, 0.4]}> <sphereGeometry /> <meshPhysicalMaterial emissive="blue" emissiveIntensity={5} color="blue" /> </mesh> </> ) } export default function App() { return ( <Canvas style={{ position: "absolute", inset: "0", touchAction: "none" }}> <Environment backgroundIntensity={0.1} backgroundRotation={[0, (90 / 180) * Math.PI, 0]} preset="studio" background blur={0.1} /> <Scene /> </Canvas> ) }
Build it in steps
- Install
pnpm add three @react-three/fiber @react-three/drei @react-three/timeline
- Minimal scene
Create App.tsx
and render a scene with two meshes.
- The scene shell
// App.tsx
import { Canvas } from '@react-three/fiber'
export default function App() {
return (
<Canvas style={{ position: "absolute", inset: "0", touchAction: "none" }}>
{/* Add <Scene /> in the next step */}
</Canvas>
)
}
- Add meshes and references
import { useRef } from 'react'
import { Mesh } from 'three'
import { Text } from '@react-three/drei'
function Scene() {
const red = useRef<Mesh>(null)
const blue = useRef<Mesh>(null)
return (
<>
<Text color="black" position-y={1} scale={0.3}>Your first timeline</Text>
<mesh ref={red} position-x={-2} position-y={-1} rotation-y={(-30/180)*Math.PI} scale={[0.2,0.2,0.4]}>
<sphereGeometry />
<meshPhysicalMaterial emissive="red" emissiveIntensity={1.5} color="red" />
</mesh>
<mesh ref={blue} position-x={2} position-y={-1} rotation-y={(20/180)*Math.PI} scale={[0.2,0.2,0.4]}>
<sphereGeometry />
<meshPhysicalMaterial emissive="blue" emissiveIntensity={5} color="blue" />
</mesh>
</>
)
}
- Add the timeline
We use useTimeline
to run a while (true)
loop, which keeps the sequence going, and each yield* action({ update: lookAt(...) })
eases the camera toward a target using a spring; once the ease completes, the next action starts.
import { useThree } from '@react-three/fiber'
import { useTimeline, action, lookAt, spring, springPresets } from '@react-three/timeline'
function Scene() {
const camera = useThree((s) => s.camera)
const red = useRef<Mesh>(null)
const blue = useRef<Mesh>(null)
useTimeline(async function* () {
while (true) {
yield* action({ update: lookAt(camera, red.current!, spring(springPresets.stiff)) })
yield* action({ update: lookAt(camera, blue.current!, spring(springPresets.stiff)) })
}
}, [])
// ...return meshes as above
}
- Mount the scene
export default function App() {
return (
<Canvas style={{ position: "absolute", inset: "0", touchAction: "none" }}>
<Scene />
</Canvas>
)
}