Building Complex Timelines using Parallel
Run multiple timelines concurrently and wait for all or race the first.
Use parallel('all' | 'race', ...)
to orchestrate concurrent behaviors. In this example we move two cubes simultaneously and continue only when both finishes.
Note
you can pass conditionally included timelines by using booleans; falsey entries are ignored. On 'race'
, non‑winning timelines are canceled and their cleanups run.
The following demo shows how the left cube constantly moves since it has to cover a larger distance while the right cube waits for the cube since both move at the same speed while the right cube has a shorter distance to travel.
import { Canvas, useThree } from '@react-three/fiber' import { useRef } from 'react' import { Mesh, Vector3 } from 'three' import { useTimeline, action, parallel, transition, velocity } from '@react-three/timeline' async function* moveTo(ref: React.RefObject<Mesh>, to: Vector3) { if (!ref.current) return yield* action({ update: transition(ref.current.position, to, velocity(1, 5)) }) } function Scene() { const cubeA = useRef<Mesh>(null) const cubeB = useRef<Mesh>(null) useTimeline(async function* () { while (true) { yield* parallel('all', moveTo(cubeA, new Vector3(-3.5, 0.5, 0)), moveTo(cubeB, new Vector3(1.5, -0.5, 0))) // Then nudge both back to origin, racing the first finisher yield* parallel('all', moveTo(cubeA, new Vector3(0, 0, 0)), moveTo(cubeB, new Vector3(0, 0, 0))) } }, []) return ( <> <mesh ref={cubeA} position={[-2,0,0]}> <boxGeometry /> <meshStandardMaterial color="hotpink" /> </mesh> <mesh ref={cubeB} position={[2,0,0]}> <boxGeometry /> <meshStandardMaterial color="orange" /> </mesh> <ambientLight intensity={0.5} /> <directionalLight position={[3,3,3]} intensity={1} /> </> ) } export default function App() { return ( <Canvas style={{ position: "absolute", inset: "0", touchAction: "none" }}> <Scene /> </Canvas> ) }
Key ideas
- parallel('all', ...) waits for all child timelines to finish.
- parallel('race', ...) continues as soon as the first child finishes and cancels the rest.
Build it in steps
- Define a reusable
moveTo
sub-timeline (what it is): a small timeline that transitions a mesh position to a target usingtransition(..., spring())
.
export async function* moveTo(ref: React.RefObject<Mesh>, to: Vector3) {
if (!ref.current) return
yield* action({ update: transition(ref.current.position, to, velocity(1, 5)) })
}
- Create a
Scene
with two cubes and lights
import { useRef } from 'react'
import { Mesh } from 'three'
function Scene() {
const cubeA = useRef<Mesh>(null)
const cubeB = useRef<Mesh>(null)
return (
<>
<mesh ref={cubeA} position={[-2, 0, 0]}>
<boxGeometry />
<meshStandardMaterial color="hotpink" />
</mesh>
<mesh ref={cubeB} position={[2, 0, 0]}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
<ambientLight intensity={0.5} />
<directionalLight position={[3, 3, 3]} intensity={1} />
</>
)
}
- Add the timeline using
parallel('all')
thenparallel('race')
import { useTimeline, parallel } from '@react-three/timeline'
import { Vector3 } from 'three'
function Scene() {
const cubeA = useRef<Mesh>(null)
const cubeB = useRef<Mesh>(null)
useTimeline(async function* () {
while (true) {
yield* parallel('all', moveTo(cubeA, new Vector3(-3.5, 0.5, 0)), moveTo(cubeB, new Vector3(1.5, -0.5, 0)))
yield* parallel('all', moveTo(cubeA, new Vector3(0, 0, 0)), moveTo(cubeB, new Vector3(0, 0, 0)))
}
}, [])
// ...return cubes and lights from step 2
}
- Mount the scene in a Canvas
import { Canvas } from '@react-three/fiber'
export default function App() {
return (
<Canvas style={{ position: 'absolute', inset: '0', touchAction: 'none' }}>
<Scene />
</Canvas>
)
}