Navigate

Search topics across all sections

GitHub
Text & HTML

HTML Overlay

Imagine wearing augmented reality glasses. You look at a building and a floating info card appears next to it -- the name, the address, a star rating. You turn your head, the label follows the building perfectly. That is exactly what the Html component does: it pins real HTML content to 3D positions in your scene.

You place an Html component with a button inside your 3D scene. The button appears at the right position, but when you click it... nothing happens. The click seems to pass straight through the button into the canvas below.

terminal
onClick handler never fires. Pointer events are being captured by the canvas layer underneath the HTML overlay.

Real-world

Think of AR glasses. When you look at a real-world object through the lenses, you see digital labels floating next to it. The labels are not part of the physical world -- they are a separate layer projected onto your view. Move the object, and the label follows.

The Html component works the same way. Your 3D objects live on the WebGL canvas. Html creates a separate DOM layer on top and projects regular HTML elements to match 3D positions every frame. The HTML is not inside the 3D world -- it is a sticker on the glass that tracks what is behind it.

3D Object

A mesh in your scene

Html Component

Attach as child

DOM Overlay

Real HTML on top

AR Label!

Follows the object

Attaching Your First AR Label

The Html component from drei makes this surprisingly simple. Place it inside any mesh or group, and it will track that object's position automatically.

Step 1: Place Html inside a mesh

BasicLabel.tsxTSX
<mesh position={[2, 1, 0]}>
  <sphereGeometry args={[0.5]} />
  <meshStandardMaterial color="tomato" />
  <Html center>
    <div className="label">This is a sphere</div>
  </Html>
</mesh>

By nesting Html inside the mesh, the label automatically follows wherever the mesh goes. The center prop keeps the label centered on the object rather than anchored to its top-left corner.

Step 2: Make it feel embedded with transform

TransformLabel.tsxTSX
<Html transform distanceFactor={10}>
  <div className="panel">
    <h3>Info Panel</h3>
    <p>Scales with the scene</p>
  </div>
</Html>

Without transform, the label stays a fixed pixel size no matter how far the camera is. With transform, it scales and rotates with the 3D perspective -- like a card that is actually in the scene rather than taped to your glasses.

Step 3: Add interactivity and occlusion

InteractiveLabel.tsxTSX
<Html
  center
  occlude={[wallRef]}
  style={{ pointerEvents: 'auto' }}
>
  <button onClick={handleClick}>
    Click me
  </button>
</Html>

Two crucial extras: pointerEvents lets clicks reach your HTML elements, and occlude hides the label when it goes behind a 3D object. Without occlude, labels awkwardly float on top of walls they should be hidden behind.

What you just learned

Html creates real DOM elements projected onto 3D positions -- like AR labels on glasses.

Nest Html inside a mesh or group and it automatically tracks that object's position.

Use the transform prop to make labels scale and rotate with the 3D scene perspective.

Add style={{ pointerEvents: 'auto' }} to make buttons and inputs clickable inside Html.

Use the occlude prop to hide labels behind 3D objects for realistic depth behavior.

Question

If Html creates real DOM elements, does that mean you can use any React library inside it? What about a chart library, a form, or even a video player?

Think about it...

You want to add a tooltip to 200 trees in a forest scene. Each tree should show its species name on hover. Which approach would you use?

Hint: Think about how many DOM elements the browser can update at 60fps...

Try These Challenges

Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.

Try This!

Beginner

Toggle showLabels off — labels vanish

Try This!

Beginner

Set sphereSize to 1 — big planets

Try This!

Beginner

Max bobSpeed — bouncy spheres

These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.

1
HTML content is not clickable
Missing pointer-events on the Html wrapper
Don't do this
InteractiveHtml.tsxTSX
<Html position={[0, 1.5, 0]} center>
  <button onClick={() => console.log('clicked')}>
    Click me
  </button>
</Html>
The Html component's wrapper often has pointer-events: none by default, which prevents clicks from reaching your content. Add style={{ pointerEvents: 'auto' }} to the Html component to re-enable mouse interactions on your buttons and inputs.
2
HTML label feels disconnected from the scene
Not using the transform prop for embedded panels
Don't do this
TransformHtml.tsxTSX
<Html position={[0, 1, 0]}>
  <div className="info-panel">
    <h3>Product Details</h3>
  </div>
</Html>
Without the transform prop, Html renders as a screen-space overlay that always faces the camera at a fixed pixel size. Use transform to make it scale and rotate with the 3D scene, as if it were a flat plane floating in 3D space. Combine with distanceFactor to control the base scale.
3
HTML shows through objects behind it
Not using the occlude prop for depth-correct rendering
Don't do this
OccludedHtml.tsxTSX
<mesh ref={wallRef}>
  <boxGeometry args={[4, 4, 0.2]} />
  <meshStandardMaterial />
</mesh>

<Html position={[0, 0, -2]}>
  <p>I should be hidden behind the wall</p>
</Html>
Html elements are DOM overlays that render on top of the WebGL canvas regardless of depth. Use the occlude prop to enable depth-testing. Pass an array of mesh refs, or use occlude='blending' for a smooth fade when the label goes behind objects.

Best Practices

Limit Html Count

Each Html component creates a DOM element updated every frame. Keep the total under 20 to avoid layout thrashing. For many labels, use the flat Text component or a shared Html pattern.

Use wrapperClass

Style the outer positioning div with wrapperClass instead of inline styles. This keeps your CSS organized and avoids unnecessary object recreation on re-renders.

Always Occlude

Use the occlude prop whenever labels should hide behind objects. Without it, labels float on top of everything, which breaks the spatial illusion completely.

Set Pointer Events Explicitly

Always add pointerEvents: 'auto' on interactive Html elements. Without it, clicks pass through to the canvas and your buttons silently do nothing.