Just track it

import { ThreeDOM } from "portalgl/three"

// renderer is initialized; container and element are DOM nodes.
const views = new ThreeDOM({ renderer, container })
const view = views.addView(element, { scene, camera })

views is a ThreeDOM instance. It connects your Three.js renderer to the page. addView attaches a scene and camera to an HTML element.

Scroll or resize your browser. The dashed outline is HTML. The cube is Three.js.

React example

Start with an initialized Three.js WebGPURenderer using alpha: true, your scene, and a perspective camera. PortalGL manages DOM measurements. This component creates ThreeDOM, registers the element, and runs the render loop.

"use client"

import { useEffect, useRef } from "react"
import { ThreeDOM } from "portalgl/three"
import type { WebGPURenderer, Scene, PerspectiveCamera } from "three/webgpu"

type Props = {
  renderer: WebGPURenderer // Already initialized with await renderer.init()
  scene: Scene
  camera: PerspectiveCamera
}

export default function TrackedView({ renderer, scene, camera }: Props) {
  const container = useRef<HTMLDivElement>(null)
  const element = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const views = new ThreeDOM({
      renderer, container: container.current!,
    })
    views.addView(element.current!, { scene, camera })

    renderer.setAnimationLoop(time => {
      views.advance(time)
      views.render()
    })

    return () => {
      renderer.setAnimationLoop(null)
      views.destroy()
    }
  }, [renderer, scene, camera])

  return (
    <div ref={container} className="relative">
      <div ref={element} className="h-64 w-full" />
    </div>
  )
}

Use the outer div as the full-page wrapper, starting at the document’s top-left. Keep the renderer, scene, and camera stable between renders. This component owns the renderer’s animation loop while mounted; its cleanup stops that loop and releases tracking. The caller still owns the renderer and scene resources.

Manual JavaScript: scene setup

To use your own Three.js scene directly, run this in the browser after the elements mount. Create your scene, camera, and a WebGPURenderer with alpha: true first.

<div id="demo" style="position: relative">
  <div id="cube" style="width: 75%; height: 256px; margin: auto"></div>
</div>
import { ThreeDOM } from "portalgl/three"

await renderer.init()

// A positioned wrapper containing your page and tracked elements.
const container = document.querySelector<HTMLElement>("#demo")!
const element = document.querySelector<HTMLElement>("#cube")!
const views = new ThreeDOM({ renderer, container })
const view = views.addView(element, { scene, camera })
await views.prepare()

renderer.setAnimationLoop(time => {
  views.advance(time)
  views.render()
})
Manual JavaScript: bounds and cleanup

The React effect above stops the loop and releases ThreeDOM and its measurements. With manual setup, perform the same cleanup below, then dispose the Three.js resources you own.

// Inside your loop, after views.advance(time):
const rect = view.tracked.rect // { top, left, width, height, ... } or null

// On teardown:
renderer.setAnimationLoop(null)
views.destroy()
// Dispose your geometries, materials, textures, and renderer too.

Preparing 3D views…