Skip to content

Scroll-driven experiences

A scroll-driven sequence is deterministic: normalized page progress maps directly onto timeline time.

Basic hook

tsx
function useScrollProgress(section: HTMLElement | null) {
  const [progress, setProgress] = useState(0)

  useEffect(() => {
    if (!section) return
    const update = () => {
      const rect = section.getBoundingClientRect()
      const distance = rect.height - window.innerHeight
      setProgress(Math.min(1, Math.max(0, -rect.top / distance)))
    }
    update()
    window.addEventListener('scroll', update, { passive: true })
    window.addEventListener('resize', update)
    return () => {
      window.removeEventListener('scroll', update)
      window.removeEventListener('resize', update)
    }
  }, [section])

  return progress
}

Viewer integration

tsx
<ModelLabViewer
  src="/models/galaxy.glb"
  preset={preset}
  playSequence={false}
  sequenceProgress={progress}
  effect={{
    type: 'texture-vortex',
    progress,
    intensity: 0.9,
  }}
/>

UX guidance

  • Keep the scroll section long enough for the movement to be readable.
  • Disable orbit controls while the scroll timeline owns the camera.
  • Respect prefers-reduced-motion by using a static snapshot or a reduced sequence.
  • Avoid forcing scroll position.

Released under the MIT License.