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

  1. Define a reusable moveTo sub-timeline (what it is): a small timeline that transitions a mesh position to a target using transition(..., 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)) })
}
  1. 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} />
    </>
  )
}
  1. Add the timeline using parallel('all') then parallel('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
}
  1. 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>
  )
}