Unlike the standard WebGLRenderer which draws pixels to a canvas, CSS3DRenderer takes real HTML elements and applies CSS 3D transforms to position them in 3D space. This means YouTube iframes remain fully functional — users can play, pause, and interact with the videos while they float in a 3D scene.
The architecture is simple: create a CSS3DObject wrapper around each iframe div, position them in a square formation (front, right, back, left faces), add OrbitControls for camera interaction, and enable auto-rotation for a polished attract-mode effect.
CSS3DRenderer vs WebGLRenderer
WebGLRenderer draws everything as pixels on a canvas — great for 3D models, particles, and shaders, but HTML content becomes a flat texture that loses interactivity.
CSS3DRenderer keeps HTML elements as real DOM nodes and uses transform: matrix3d() CSS to position them in 3D. The elements stay interactive — clicks, hovers, and inputs all work. The tradeoff is you can't mix it with WebGL objects (no 3D models or lighting), but for video walls, dashboards, and UI in 3D space, it's perfect.
// CSS3DRenderer — renders HTML elements in 3D space
var renderer = new CSS3DRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Wrap any HTML element as a 3D object
var div = document.createElement('div');
div.innerHTML = '<iframe src="..."></iframe>';
var object = new CSS3DObject(div);
object.position.set(0, 0, 240); // Place in 3D space
scene.add(object);
Building the Video Cube
Each face of the cube is a YouTube iframe wrapped in a CSS3DObject. We position four faces in a square: front at z=+240, right at x=+240 (rotated 90°), back at z=-240 (rotated 180°), and left at x=-240 (rotated -90°). The rotation ensures each iframe faces outward.
// Position 4 videos in a square formation
group.add(Element('VIDEO_ID_1', 0, 0, 240, 0)); // Front face
group.add(Element('VIDEO_ID_2', 240, 0, 0, Math.PI/2)); // Right face
group.add(Element('VIDEO_ID_3', 0, 0, -240, Math.PI)); // Back face
group.add(Element('VIDEO_ID_4', -240, 0, 0, -Math.PI/2)); // Left face
The 240-unit offset is half the iframe width (480px / 2). This places each face exactly at the edge of the cube so they form a seamless box when viewed from outside.
The Blocker Pattern
There's a subtle UX problem: when you drag to rotate the camera, the mouse passes over the YouTube iframes, which capture the mouse events and prevent the drag from working. The solution is a transparent <div id="blocker"> that covers the entire viewport during camera drag, blocking iframe interaction.
// Show blocker during drag to prevent iframe stealing mouse events
controls.addEventListener('start', function() {
blocker.style.display = ''; // Show — blocks iframe clicks
});
controls.addEventListener('end', function() {
blocker.style.display = 'none'; // Hide — iframes clickable again
});
OrbitControls with Constraints
We lock the vertical orbit angle so the camera can only rotate horizontally around the cube (no looking from above or below). This is done by setting both minPolarAngle and maxPolarAngle to Math.PI / 2 (90°). Auto-rotation is enabled for a hands-free showcase effect.
Key Takeaways
CSS3DRenderer — renders real HTML elements in 3D space. Elements stay interactive (clicks, inputs work).
CSS3DObject — wraps any DOM element for 3D positioning and rotation.
Blocker pattern — transparent overlay during drag prevents iframes from stealing mouse events.
Polar angle locking — constrain OrbitControls to horizontal-only rotation with min/maxPolarAngle.
Auto-rotation — set controls.autoRotate = true for a polished attract mode.
Group positioning — use offset = width/2 and rotation increments of π/2 to form a cube.
COMPLETE SOURCE CODE
Copy this code into an HTML file and open it in your browser to see it in action:
HTML / Three.js
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js - 3D Rendering YouTube Videos</title>
<style>
/* Black background, no margins — full-screen 3D scene */
body { background: #000; color: #fff; margin: 0; overflow: hidden; }
/* Blocker: transparent overlay that covers the screen during camera drag.
This prevents YouTube iframes from capturing mouse events while rotating. */
#blocker {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
z-index: 10; display: none;
}
</style>
</head>
<body>
<!-- Container for the CSS3D renderer output -->
<div id="container"></div>
<!-- Stop/Start button — lets user pause rotation to watch a video -->
<button id="toggleBtn" style="position:fixed;bottom:20px;left:50%;transform:translateX(-50%);padding:12px 28px;background:#2ad1d6;color:#000;font-weight:bold;border:none;border-radius:9999px;cursor:pointer;z-index:100;font-size:14px;">⏸ Stop Rotation</button>
<!-- Invisible blocker div — shown during drag to prevent iframe mouse capture -->
<div id="blocker"></div>
<!-- Load Three.js, OrbitControls, and CSS3DRenderer from CDN -->
<script src="https://cdn.jsdelivr.net/npm/three@latest/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@latest/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@latest/examples/js/renderers/CSS3DRenderer.js"></script>
<script>
// ============================================================
// ELEMENT FACTORY
// Creates a CSS3DObject containing a YouTube iframe.
// Each "element" is one face of our video cube.
//
// Parameters:
// id — YouTube video ID (the part after youtube.com/embed/)
// x, y, z — 3D position of this face
// ry — rotation around Y axis (radians) so the face points outward
// ============================================================
function Element(id, x, y, z, ry) {
// Create a container div for the iframe
var div = document.createElement('div');
div.style.width = '480px';
div.style.height = '360px';
div.style.backgroundColor = '#000';
// Create the YouTube iframe embed
var iframe = document.createElement('iframe');
iframe.style.width = '480px';
iframe.style.height = '360px';
iframe.style.border = '0px';
iframe.src = 'https://www.youtube.com/embed/' + id + '?rel=0';
div.appendChild(iframe);
// Wrap the div as a CSS3DObject — this makes it positionable in 3D space
var object = new THREE.CSS3DObject(div);
object.position.set(x, y, z); // Place in 3D world
object.rotation.y = ry; // Rotate to face outward
return object;
}
// ============================================================
// SCENE SETUP
// CSS3DRenderer uses a regular Three.js scene and camera,
// but renders HTML elements with CSS transforms instead of WebGL.
// ============================================================
var camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 5000);
camera.position.set(0, 0, 800); // Start 800 units back from center
var scene = new THREE.Scene();
// CSS3DRenderer — renders DOM elements in 3D using CSS transforms
// Unlike WebGLRenderer, elements remain interactive (clickable, scrollable)
var renderer = new THREE.CSS3DRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
// ============================================================
// BUILD THE VIDEO CUBE
// Place 4 YouTube videos in a square formation.
// Each face is offset by 240px (half of 480px width) and
// rotated by 90 degrees (PI/2 radians) to face outward.
// ============================================================
var group = new THREE.Group();
// Front face — no rotation needed (faces the camera by default)
group.add(Element('67Ii__vPouE', 0, 0, 240, 0));
// Right face — rotated 90 degrees, offset on X axis
group.add(Element('_n8Yb_j5E2o', 240, 0, 0, Math.PI / 2));
// Back face — rotated 180 degrees, offset on negative Z
group.add(Element('STRk1CWK8JQ', 0, 0, -240, Math.PI));
// Left face — rotated -90 degrees, offset on negative X
group.add(Element('mhuxhXU9PPY', -240, 0, 0, -Math.PI / 2));
scene.add(group);
// ============================================================
// ORBIT CONTROLS
// Lets the user drag to rotate the camera around the video cube.
// We lock vertical movement (polar angle) so it only rotates
// horizontally, and enable auto-rotation for a showcase effect.
// ============================================================
var controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.rotateSpeed = 2; // How fast dragging rotates
controls.autoRotate = true; // Slowly spin when not dragging
controls.autoRotateSpeed = 1.5; // Degrees per second
controls.enableZoom = false; // Disable scroll zoom
controls.enableDamping = true; // Smooth deceleration after drag
controls.dampingFactor = 0.05;
// Lock vertical angle to exactly 90 degrees (horizontal only)
controls.minPolarAngle = Math.PI / 2;
controls.maxPolarAngle = Math.PI / 2;
// ============================================================
// BLOCKER PATTERN
// YouTube iframes capture mouse events (clicks, drags).
// When the user starts dragging to rotate the camera, we show
// a transparent overlay that blocks the iframes from stealing
// the mouse. When dragging ends, we hide it so videos are
// clickable again.
// ============================================================
var blocker = document.getElementById('blocker');
controls.addEventListener('start', function () {
blocker.style.display = ''; // Show blocker — blocks iframe interaction
});
controls.addEventListener('end', function () {
blocker.style.display = 'none'; // Hide blocker — iframes clickable again
});
// ============================================================
// STOP/START BUTTON
// Lets the user pause auto-rotation to watch a video,
// then resume when they're done.
// ============================================================
var toggleBtn = document.getElementById('toggleBtn');
toggleBtn.addEventListener('click', function () {
controls.autoRotate = !controls.autoRotate;
toggleBtn.textContent = controls.autoRotate ? '⏸ Stop Rotation' : '▶ Start Rotation';
});
// ============================================================
// RENDER LOOP
// Updates OrbitControls (needed for auto-rotate and damping)
// and renders the scene every frame.
// ============================================================
function animate() {
requestAnimationFrame(animate);
controls.update(); // Update auto-rotation and damping
renderer.render(scene, camera); // Render the 3D CSS scene
}
animate();
// ============================================================
// WINDOW RESIZE
// Keep the scene responsive when the browser window changes size.
// ============================================================
window.addEventListener('resize', function () {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>