Building Graph Timelines

Model behavior as states with transitions driven by time or conditions.

Graph timelines let you model states and transitions. We'll switch between two states (red and blue) depending on the mouse position, using promise-based triggers instead of React state.

In the following example, you can move the mouse to the left or right of the canvas to trigger a state transition.

import { Canvas, useThree } from '@react-three/fiber'
import { useRef } from 'react'
import { Mesh } from 'three'
import { useTimelineGraph, action, lookAt, spring } from '@react-three/timeline'

function mouseOnSide(side: 'left' | 'right'): Promise<void> {
return new Promise((resolve) => {
function onMove(e: MouseEvent) {
const onRight = e.clientX / window.innerWidth > 0.5
if ((side === 'right' && onRight) || (side === 'left' && !onRight)) {
window.removeEventListener('mousemove', onMove)
resolve()
}
}
window.addEventListener('mousemove', onMove)
})
}

function Scene() {
const camera = useThree((s) => s.camera)
const red = useRef<Mesh>(null)
const blue = useRef<Mesh>(null)

useTimelineGraph(
'red',
{
red: {
timeline: () => action({ update: lookAt(camera, red.current!, spring()) }),
transitionTo: { blue: { whenPromise: () => mouseOnSide('right') } },
},
blue: {
timeline: () => action({ update: lookAt(camera, blue.current!, spring()) }),
transitionTo: { red: { whenPromise: () => mouseOnSide('left') } },
},
},
[camera],
)

return (

<>
<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>
<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

  • Define a stateMap with timeline for each state and transitionTo conditions.
  • Transitions can be driven by promises (e.g., “resolve when mouse is on the right side”).

Extras:

  • You can return a state name from a state's timeline to jump programmatically.
  • You can set transitionTo: { finally: 'someState' } to always transition after a state's timeline completes.

Build it in steps

  1. Create a reusable promise trigger for either side
function mouseOnSide(side: 'left' | 'right'): Promise<void> {
  return new Promise((resolve) => {
    function onMove(e: MouseEvent) {
      const onRight = e.clientX / window.innerWidth > 0.5
      if ((side === 'right' && onRight) || (side === 'left' && !onRight)) {
        window.removeEventListener('mousemove', onMove)
        resolve()
      }
    }
    window.addEventListener('mousemove', onMove)
  })
}
  1. Start the graph with inline states (no separate variable) and render the scene. The state shape is inferred by useTimelineGraph.
useTimelineGraph(
  'red',
  {
    red: {
      timeline: () => action({ update: lookAt(camera, red.current!, spring()) }),
      transitionTo: { blue: { whenPromise: () => mouseOnSide('right') } },
    },
    blue: {
      timeline: () => action({ update: lookAt(camera, blue.current!, spring()) }),
      transitionTo: { red: { whenPromise: () => mouseOnSide('left') } },
    },
  },
  [camera],
)