Skip to content

Track which objects users click in three.js

To track clicks on objects in three.js, you need to do two things: turn each click into a raycast that finds the object under the pointer, and store the object’s name somewhere you can count it. You can build this yourself in about ten lines, then decide whether you also want to build everything around it.

Convert the click to normalized device coordinates, cast a ray from the camera, and report the nearest hit:

import * as THREE from "three";
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
renderer.domElement.addEventListener("click", (event) => {
const rect = renderer.domElement.getBoundingClientRect();
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const [hit] = raycaster.intersectObjects(scene.children, true);
if (!hit) return;
sendToYourAnalytics({ object: hit.object.name, point: hit.point.toArray() });
});

This is enough to log a click. A usable report also needs:

  • Delivery: batching events, and flushing them when the visitor closes the tab.
  • Storage and ranking: somewhere to keep the events and a way to rank objects across sessions.
  • Misses: clicks that hit nothing are often the most useful ones, and the snippet above drops them.
  • Pointer lock: in first-person scenes the cursor is hidden, so the aim point is the centre of the screen and not event.clientX.

@uptimizr/three does the raycasting and delivery for you and adds camera, pointer and performance capture:

Terminal window
npm install @uptimizr/three three
import { trackScene } from "@uptimizr/three";
const client = trackScene(scene, camera, renderer, {
projectId: "your-project",
endpoint: "https://collect.example.com",
});

Every click or tap that hits geometry now emits a mesh_interaction event with the mesh name and the world-space hit point. No further setup is needed. The endpoint is a collector you run yourself.

Results are only as good as your object names. The hit is reported using the name of the mesh the ray struck, not its parent group, so name the meshes themselves:

gltf.scene.getObjectByName("Cube012").name = "product-hero";

Procedural or clone-suffixed names such as Box.001 and Box.002 split one object across several rows in your ranking, and an unnamed mesh reports an empty name. Give every interactive mesh a deliberate, stable name.

The dashboard shows the ranking as the most-interacted meshes view, and the query API returns the same data:

GET /api/v1/meshes/top?since=...&limit=20
x-api-key: your-project-api-key

Clicks only show what people successfully acted on. These signals show what they tried and missed:

You want to know Use
What people click that does nothing GET /api/v1/clicks/dead reports clicks that hit no geometry
Where people get frustrated GET /api/v1/clicks/rage reports rapid repeated clicks on one mesh
What looks interactive but gets no click hoverDwell
What’s on screen but ignored Blind spots, using meshVisibility

Hover and visibility capture are off by default. Turn them on when you need them:

const client = trackScene(scene, camera, renderer, {
projectId: "your-project",
endpoint: "https://collect.example.com",
capture: { hoverDwell: true, meshVisibility: true },
});