Embed DOM Elements in Three.js with CSS3DRenderer
Integrating standard HTML elements like videos, forms, and
interactive buttons into a 3D WebGL scene can be challenging because
standard WebGL renderers draw on a flat canvas. This article explains
how to use Three.js’s CSS3DRenderer to transform standard
DOM elements into 3D space, mapping them alongside your WebGL objects
while preserving their native HTML interactivity and styling.
Understanding the CSS3DRenderer
While the standard WebGLRenderer projects 3D geometry onto a 2D
canvas using shaders, the CSS3DRenderer utilizes CSS3 3D
transforms (transform-style: preserve-3d) to translate,
rotate, and scale HTML elements. By syncing the CSS3D camera with your
WebGL camera, DOM elements appear perfectly aligned within the 3D space,
allowing users to select text, click buttons, and interact with inputs
as if they were on a flat web page.
Step 1: Import the Necessary Modules
To get started, you need to import the standard Three.js library
along with CSS3DRenderer and CSS3DObject from
the examples folder.
import * as THREE from 'three';
import { CSS3DRenderer, CSS3DObject } from 'three/examples/jsm/renderers/CSS3DRenderer.js';Step 2: Set Up the HTML and CSS Renderers
For a fully integrated scene where WebGL models and HTML elements coexist, you must create and layer two renderers: the WebGLRenderer and the CSS3DRenderer.
// Set up scene, camera
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 500;
// Set up CSS3D Renderer
const cssRenderer = new CSS3DRenderer();
cssRenderer.setSize(window.innerWidth, window.innerHeight);
cssRenderer.domElement.style.position = 'absolute';
cssRenderer.domElement.style.top = '0';
cssRenderer.domElement.style.pointerEvents = 'none'; // Allows clicks to pass through if necessary
document.body.appendChild(cssRenderer.domElement);
// Set up WebGL Renderer
const webglRenderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
webglRenderer.setSize(window.innerWidth, window.innerHeight);
webglRenderer.domElement.style.position = 'absolute';
webglRenderer.domElement.style.top = '0';
webglRenderer.domElement.style.zIndex = '1';
document.body.appendChild(webglRenderer.domElement);To ensure users can click on the HTML elements, you may need to swap
the zIndex or manage the CSS pointer-events
property so that the interactive layer sits correctly in the pointer
hierarchy.
Step 3: Create and Embed the DOM Element
Next, define the HTML element in JavaScript, apply standard CSS
styles, wrap it in a CSS3DObject, and add it to your
scene.
// Create the DOM element
const element = document.createElement('div');
element.style.width = '300px';
element.style.height = '200px';
element.style.background = '#007acc';
element.style.color = '#white';
element.style.padding = '20px';
element.style.boxSizing = 'border-box';
element.innerHTML = `
<h3>Interactive 3D Card</h3>
<p>This is a native HTML element inside a Three.js scene.</p>
<button id="myButton" style="padding: 10px; cursor: pointer;">Click Me</button>
`;
// Add interactivity directly to the element
element.querySelector('#myButton').addEventListener('click', () => {
alert('Button clicked inside 3D space!');
});
// Convert DOM element to a 3D Object
const cssObject = new CSS3DObject(element);
cssObject.position.set(0, 0, 0);
scene.add(cssObject);Step 4: The Animation Loop
To keep the 3D transforms synchronized, you must update both the WebGL renderer and the CSS3D renderer inside your requestAnimationFrame loop.
function animate() {
requestAnimationFrame(animate);
// Optional: Rotate the CSS3D object to demonstrate 3D spatial positioning
cssObject.rotation.y += 0.01;
// Render both scenes
webglRenderer.render(scene, camera);
cssRenderer.render(scene, camera);
}
animate();Handling Resize Events
To prevent distortion when resizing the browser window, update the aspect ratio of the camera and reset the sizes for both renderers.
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
webglRenderer.setSize(window.innerWidth, window.innerHeight);
cssRenderer.setSize(window.innerWidth, window.innerHeight);
});