šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Raycasting & Interaction in Three.js 3D WebGL

Learn about Raycasting & Interaction in this comprehensive Three.js 3D WebGL tutorial. Explore the THREE.Raycaster class and how React Three Fiber simplifies 3D events.

⚔ Total XP: 0|šŸ’» threejs XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core raycasting concepts.

Quick Quiz //

What does THREE.Raycaster.intersectObjects() return when the ray hits multiple overlapping objects?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

Listen up. If you're building modern applications, understanding Raycasting & Interaction in Three.js 3D WebGL is non-negotiable. This is where simple logic turns into intelligent behavior.

1The Interactivity Problem in WebGL

So far, our 3D worlds have been purely visual. But what if you want to click on a 3D object to select it or trigger an event?

A WebGL canvas is just a single <canvas> element containing a flat grid of colored pixels — there's no way to attach a native onClick handler to an individual mesh the way you would to a DOM element, because as far as the browser is concerned, the cube and the background are all just pixels drawn by the GPU.

To make 3D objects clickable, you need to work backwards: given a 2D click position on the canvas, figure out which 3D object (if any) exists behind that pixel. That's the job of raycasting, which we'll build in this lesson.

āœ•
—
+
// šŸ–±ļø Adding interactivity
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

2Setting Up THREE.Raycaster and Vector2

You can't just attach an 'onClick' event to a WebGL canvas easily, because it's just a flat 2D image of pixels. We need to cast a Ray.

THREE.Raycaster is the class built specifically for this: given an origin point and a direction, it can test for intersections against any object (or array of objects) in your scene, returning the ones the ray actually passes through. THREE.Vector2 is just a lightweight (x, y) container used to hold the mouse's normalized coordinates before they're handed to the raycaster.

You typically create one Raycaster instance and reuse it every frame or on every click, rather than instantiating a new one each time — there's no need to recreate it since setFromCamera() simply overwrites its origin and direction.

āœ•
—
+
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

3Converting Mouse Position to Normalized Device Coordinates

Here is how it works: you convert the mouse's 2D screen coordinates into normalized device coordinates (-1 to +1).

Browser mouse events give you clientX/clientY in pixels, but the Raycaster expects coordinates in NDC space, where (-1, -1) is the bottom-left of the canvas and (1, 1) is the top-right, with (0, 0) at dead center. Dividing by innerWidth/innerHeight and remapping to the -1..1 range performs that conversion.

Notice the Y axis is inverted (the leading minus sign) — screen coordinates increase downward while NDC/clip space Y increases upward, so without that flip your raycasts would be vertically mirrored from where the user is actually pointing.

āœ•
—
+
window.addEventListener('mousemove', (event) => {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
});
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

4Firing the Ray and Reading Intersections

Then, you tell the Raycaster to shoot a ray from the camera through that mouse coordinate. It returns an array of all objects the ray hit!

setFromCamera(mouse, camera) computes the ray's origin and direction using the camera's position and projection, effectively projecting a line from the eye through that point on the near plane and out into the scene. intersectObjects() then tests that ray against every object you pass in (optionally recursing into children) and returns an array of intersection results, sorted from nearest to farthest.

Each entry in that array is more than just the object — it also includes the exact intersection point in world space, the distance from the ray origin, and (for meshes) the face that was hit, which is useful for things like placing decals or reading UV coordinates at the click point.

āœ•
—
+
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children);

if (intersects.length > 0) {
  console.log('Hit:', intersects[0].object);
}
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

5Vanilla Three.js Boilerplate vs React Three Fiber

Doing this in vanilla Three.js requires a lot of boilerplate. React Three Fiber handles ALL of this for you automatically!

In vanilla Three.js you're responsible for wiring up the mousemove/click listeners, converting coordinates to NDC, creating and updating the Raycaster every frame, and manually diffing which object was previously hovered versus currently hovered to fire enter/leave-style events yourself.

R3F builds a single shared raycaster into its render loop and automatically tests it against every mesh registered in the scene graph, so it can dispatch familiar-feeling pointer events without you writing any raycasting code by hand.

āœ•
—
+
// In vanilla: 20 lines of code.
// In R3F: Just use standard React events!
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

6Declarative Pointer Events on Meshes

In R3F, you can attach standard React event listeners like onClick, onPointerOver, and onPointerOut directly to your meshes!

Under the hood these aren't real DOM events — R3F's internal raycaster runs each frame, determines which mesh (if any) is currently under the pointer, and synthesizes a React-like event object that gets passed to your handler, complete with things like intersection point and distance.

Because these props live directly on the JSX element, event handling stays colocated with the object it affects, which is a big ergonomic win over manually cross-referencing intersected objects against a lookup table in vanilla Three.js.

āœ•
—
+
<mesh 
  onClick={(e) => console.log('Clicked!')} 
  onPointerOver={(e) => setHovered(true)}
  onPointerOut={(e) => setHovered(false)}
>
  <boxGeometry />
</mesh>
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

7Choosing the Right Event Prop for Clicks

In React Three Fiber, if you want an action to occur when the user clicks on a 3D sphere, which prop do you use on the <mesh> tag?

The answer is onClick, which behaves like its DOM namesake: it fires once the raycaster determines a pointer-down-then-up sequence happened over that specific mesh. There's no onIntersect or onRaycast prop — R3F deliberately mirrors familiar DOM event names (onClick, onPointerOver, onPointerOut, onPointerMove, onWheel, and more) so you can reuse existing React mental models instead of learning a raycasting-specific API.

Because these are just props, you can pass any function reference — an inline arrow function, a memoized callback, or a handler shared across multiple meshes — exactly as you would with a regular DOM element.

āœ•
—
+
<mesh ???={() => console.log('Boom')} />
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

8Building a Fully Interactive Hover-and-Click Scene

Let's see it in action! Below is a fully interactive 3D scene. Hover over the boxes to see them change color, and click them to make them jump!

The InteractiveBox component tracks hovered and active as local React state, driven entirely by onPointerOver, onPointerOut, and onClick — no manual raycasting code appears anywhere in this component, because R3F's internal picking system handles all of it behind the scenes.

Notice that scale and color are derived directly from that state rather than imperatively mutated, which is the idiomatic R3F pattern: let React re-render the mesh's props declaratively whenever pointer interaction changes the underlying state.

āœ•
—
+
const InteractiveBox = (props) => {
  const [hovered, setHover] = React.useState(false);
  const [active, setActive] = React.useState(false);
  
  return (
    <mesh
      {...props}
      scale={active ? 1.5 : 1}
      onClick={() => setActive(!active)}
      onPointerOver={() => setHover(true)}
      onPointerOut={() => setHover(false)}
    >
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
    </mesh>
  );
};

const App = () => {
  return (
    <Canvas camera={{ position: [0, 0, 5] }}>
      <ambientLight intensity={0.5} />
      <directionalLight position={[10, 10, 10]} intensity={1} />
      <InteractiveBox position={[-1.2, 0, 0]} />
      <InteractiveBox position={[1.2, 0, 0]} />
    </Canvas>
  );
};

render(<App />);
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

9Stopping Ray Propagation with event.stopPropagation()

R3F passes an 'Event' object to these handlers just like the DOM. event.stopPropagation() stops the ray from passing through the object to hit objects behind it.

By default, a single ray can intersect and report multiple overlapping objects along its path, and R3F will fire the corresponding handler on the nearest one first, then continue notifying handlers on objects further behind it unless you explicitly stop that propagation.

Calling event.stopPropagation() inside your handler halts this bubbling-through-depth behavior, ensuring only the frontmost object the user actually clicked responds — this mirrors how stopPropagation() works for nested DOM elements, just applied to 3D depth instead of the DOM tree.

āœ•
—
+
<mesh onClick={(e) => {
  e.stopPropagation(); // Stop raycasting
  console.log('Hit front object only!');
}} />
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

10Raycasting and Interaction Recap

Amazing! You can now build fully interactive 3D interfaces, games, and data visualizations. We are reaching the advanced stages!

With raycasting under your belt, you have the core primitive behind object picking, drag-and-drop in 3D, hover tooltips, and click-to-select UI patterns — nearly every interactive Three.js application relies on the same setFromCamera / intersectObjects pattern you learned here, whether written by hand or abstracted away by React Three Fiber.

In the next lesson, we'll build on this interactivity foundation and look at shaders, which control exactly how the pixels of these objects get colored on the GPU.

āœ•
—
+
// šŸŽÆ Interactivity complete!
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

11Step-by-Step Breakdown

So far, our 3D worlds have been purely visual. But what if you want to click on a 3D object to select it or trigger an event?

You can't just attach an 'onClick' event to a WebGL canvas easily, because it's just a flat 2D image of pixels. We need to cast a Ray.

Which Three.js class is used to shoot an invisible laser beam from the camera into the 3D scene to figure out which objects the mouse is hovering over?

  • →MousePicker
  • →ClickDetector
  • →Raycaster

Here is how it works: you convert the mouse's 2D screen coordinates into normalized device coordinates (-1 to +1).

Then, you tell the Raycaster to shoot a ray from the camera through that mouse coordinate. It returns an array of all objects the ray hit!

The Raycaster returns an array of intersected objects. Which element in the array is the object closest to the camera (the one the user actually clicked on)?

  • →The last element
  • →0 (The first element)
  • →It is random

Doing this in vanilla Three.js requires a lot of boilerplate. React Three Fiber handles ALL of this for you automatically!

In R3F, you can attach standard React event listeners like onClick, onPointerOver, and onPointerOut directly to your meshes!

In React Three Fiber, if you want an action to occur when the user clicks on a 3D sphere, which prop do you use on the <mesh> tag?

  • →onIntersect
  • →onClick
  • →onRaycast

Let's see it in action! Below is a fully interactive 3D scene. Hover over the boxes to see them change color, and click them to make them jump!

R3F passes an 'Event' object to these handlers just like the DOM. event.stopPropagation() stops the ray from passing through the object to hit objects behind it.

Amazing! You can now build fully interactive 3D interfaces, games, and data visualizations. We are reaching the advanced stages!

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported.

FirefoxSupported

Fully supported.

SafariSupported

Fully supported.

EdgeSupported

Fully supported.

Accessibility (A11y)

1Provide Keyboard-Accessible Alternatives to Pointer-Only Interactions

Raycasting-based interactions (onClick, onPointerOver on meshes) are fundamentally mouse/touch-driven and have no native keyboard equivalent — for any interaction that conveys meaningful functionality (not just decorative hover effects), pair it with a real, tabbable HTML control (a button that triggers the same state change) so keyboard and screen-reader users aren't locked out.

<button onClick={() => setActive(true)} aria-label="Select the highlighted object">Select</button>

SEO Implications

  • 1

    Raycasting Logic Itself Is Invisible to Crawlers, but Its Purpose Should Be Documented

    Click-to-select and hover interactions built with THREE.Raycaster run entirely on the GPU/canvas and produce no crawlable DOM content — search engines can't 'see' what an interactive 3D product configurator does. Pair interactive 3D demos with surrounding indexable text that describes what the objects represent and what clicking or hovering does, since that's the content that actually earns search visibility.

Best Practices

Reuse a Single Raycaster and Vector2 Instance

Creating a new THREE.Raycaster() or THREE.Vector2() inside a mousemove or animation-loop callback allocates garbage on every single event or frame. Declare both once outside the loop and just mutate mouse.x/mouse.y and call setFromCamera() again on each update.

Limit intersectObjects() to the Objects That Actually Need to Be Pickable

Passing scene.children (or worse, recursing through the entire scene graph) tests every mesh against the ray, including lights' helpers, ground planes never meant to be clicked, and deeply nested groups. Maintain an explicit array of interactive objects, or use layers, so intersection tests stay cheap as the scene grows.

Frequent Bugs

THE BUG

intersectObjects() returns an empty array even though the cursor is visibly over the mesh.

THE FIX

This usually means setFromCamera(mouse, camera) was called with stale or unset mouse coordinates (e.g., before any mousemove event fired), or the object being tested isn't actually in the array passed to intersectObjects() — double check you're passing the mesh itself (or its parent group with recursive true), not an unrelated object.

Real-World Examples

Product Configurators and 3D Model Viewers

E-commerce '3D product viewer' experiences use raycasting to let shoppers click individual parts of a model (a shoe's sole, a laptop's keyboard) to swap materials or colors, with onPointerOver driving a highlight outline before the click even happens — all built on the same setFromCamera/intersectObjects pattern from this lesson.

<mesh onPointerOver={() => setHoveredPart('sole')} onClick={() => setColor('sole', selectedColor)}>

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Not reading error messages carefully

Uncaught TypeError: Cannot read properties of undefined (reading 'length') // Solution: Ensure the variable you are calling .length on is initialized as a string or an array, not undefined.

The Solution //

Most of the time, the compiler or interpreter tells you exactly what line caused the crash and why. Read stack traces from the top down to identify the root cause.

The Error //

Hardcoding sensitive credentials

// Wrong const API_KEY = 'sk-123456789'; // Correct const API_KEY = process.env.API_KEY;

The Solution //

Never hardcode API keys, passwords, or secrets in your source code. Use environment variables (.env files) to keep them secure and out of version control.

Continue Learning