3D RENDERING
YOUTUBE VIDEOS
By Shane Brumback · Published · Updated · Beginner Three.js tutorial · about 25 minutes
Put four real, clickable YouTube players on the sides of a spinning 3D cube with Three.js CSS3DRenderer. Orbit it, tap 1 to 4 to face a video, and press play right inside the 3D scene.
LIVE DEMO
A neon cube of four live YouTube players that spins on its own. Drag to orbit, scroll or pinch to zoom, tap 1 to 4 to swing round to a video, and press play on any screen.
Free Google sign-in. No membership needed.
Next tutorial
YouTube Video in Three.js: Curved Screens, VideoTexture and Glow
Take this cube further: mix CSS3D with a WebGL scene, punch a hole for real depth, add a curved VideoTexture MP4 screen, a mirror floor and neon particles.
Open the tutorial →How do you render a YouTube video in 3D with Three.js? (Quick Answer)
Use CSS3DRenderer. Wrap a YouTube embed iframe in a div, turn it into a CSS3DObject, and position and rotate it like any Three.js object. The renderer moves the real HTML element with CSS 3D transforms, so the player keeps all of its controls. WebGLRenderer cannot do this, because it draws pixels and cannot read a cross-origin YouTube iframe.
Introduction
Flat video embeds are everywhere. Putting the same embeds on the faces of a 3D cube makes a gallery, a lobby wall or a portfolio showcase that people actually play with.
This tutorial uses CSS3DRenderer, which positions regular HTML in 3D with CSS transforms instead of drawing pixels. Each face is a real YouTube iframe, so play, volume and fullscreen all work while the cube spins.
Want the video glowing inside a real WebGL scene, on a curved screen with a mirror floor? That is the next step: YouTube Video in Three.js: CSS3D Screen, VideoTexture and Glow. For the player calls themselves, see the YouTube IFrame API tutorial.
Key Concepts
- CSS3DRenderer — a Three.js renderer that moves real DOM elements in 3D with CSS matrix3d transforms. Nothing is drawn on a canvas.
- CSS3DObject — wraps one HTML element (here a div holding a YouTube iframe) so it has a position, rotation and scale in the scene.
- Cube layout — each 480 px screen is pushed 240 px out from the centre at 0, 90, 180 and 270 degrees, so four screens make the sides of a cube.
- OrbitControls with autoRotate — drag to orbit, scroll or pinch to zoom, and the camera spins slowly on its own until you pick a video.
- Drag blocker — while the camera is dragged, the iframes get pointer-events: none, so YouTube does not swallow the mouse or finger. It is handed back on release.
- Cost — every iframe is a full YouTube player. Four is fine; dozens will slow a page down. For a decorative background, use thumbnail images instead of iframes.
Frequently Asked Questions
How do you render YouTube videos in 3D with Three.js?
Use CSS3DRenderer. Put the YouTube iframe in a div, wrap it in a CSS3DObject and position it in the scene. The iframe stays fully interactive.
What is the difference between CSS3DRenderer and WebGLRenderer?
WebGLRenderer draws pixels on a canvas with the GPU and cannot show interactive HTML. CSS3DRenderer places real DOM elements in 3D with CSS transforms, so you can click and use them.
Can users still use the YouTube controls in 3D?
Yes. Each face is a real YouTube iframe, so play, pause, volume and fullscreen all work.
How do you stop the iframe grabbing the mouse while orbiting?
Listen for the OrbitControls start and end events and toggle pointer-events: none on the iframes while dragging.
Can I put a YouTube video on a curved screen or use it as a texture?
Not directly; browsers do not let WebGL read a YouTube iframe. The follow-up tutorial, YouTube Video in Three.js, shows the CSS3D plus WebGL hybrid and a real VideoTexture for your own MP4 on a curved screen.
How It Works
- Load Three.js and the add-ons — In plain words: An import map loads Three.js r160 plus OrbitControls and CSS3DRenderer from a CDN.
- Create the CSS3DRenderer — In plain words: It works like a normal renderer but outputs a div of transformed HTML. Size it to the window and add it to the page.
- Make one YouTube screen — In plain words: screen() builds a div with a YouTube embed iframe, wraps it in a CSS3DObject and places it 240 px out at the given angle, facing outward.
- Build the cube — In plain words: Four screens at 0, 90, 180 and 270 degrees go into one Group.
- Add OrbitControls and the drag blocker — In plain words: Damping and autoRotate make it glide. On start and end the body gets a dragging class that turns off iframe pointer events.
- Add the 1 to 4 buttons — In plain words: Each button sets a target angle and stops the spin. The loop eases the camera round to face that video.
- Render every frame — In plain words: The loop updates the controls and calls renderer.render, and skips work while the tab is hidden.
Code Breakdown
The important pieces of the demo, one at a time. The full file is below.
const renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, innerHeight);
document.getElementById('stage').appendChild(renderer.domElement);const R = 240;
function screen(id, angle) {
const div = document.createElement('div');
div.className = 'screen';
const iframe = document.createElement('iframe');
iframe.src = 'https://www.youtube.com/embed/' + id + '?rel=0&playsinline=1';
iframe.allow = 'autoplay; encrypted-media; picture-in-picture';
div.appendChild(iframe);
const obj = new CSS3DObject(div);
obj.position.set(Math.sin(angle) * R, 0, Math.cos(angle) * R);
obj.rotation.y = angle;
return obj;
}
const cube = new THREE.Group();
VIDEOS.forEach(([id], i) => cube.add(screen(id, i * Math.PI / 2)));
scene.add(cube);const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.autoRotate = true; controls.autoRotateSpeed = 1.2;
controls.minDistance = 600; controls.maxDistance = 3200;
// Stop the iframes catching the mouse or finger while orbiting, then hand it back.
controls.addEventListener('start', () => document.body.classList.add('dragging'));
controls.addEventListener('end', () => document.body.classList.remove('dragging'));const ui = document.getElementById('ui');
let target = null;
VIDEOS.forEach(([, name], i) => {
const b = document.createElement('button');
b.textContent = i + 1; b.title = name; b.setAttribute('aria-label', 'Face ' + name);
b.onclick = () => {
target = i * Math.PI / 2; controls.autoRotate = false; spin.checked = false;
ui.querySelectorAll('button').forEach((x) => x.classList.toggle('on', x === b));
};
ui.appendChild(b);
});
const lab = document.createElement('label');
lab.innerHTML = '<input type="checkbox" checked> Spin';
const spin = lab.querySelector('input');
spin.onchange = () => { controls.autoRotate = spin.checked; if (spin.checked) target = null; };
ui.appendChild(lab);function animate() {
requestAnimationFrame(animate);
if (document.hidden) return;
if (target !== null) {
// Ease the camera round the Y axis toward the chosen side.
const off = camera.position.clone().sub(controls.target);
let d = target - Math.atan2(off.x, off.z); d = Math.atan2(Math.sin(d), Math.cos(d));
if (Math.abs(d) < 0.002) target = null;
off.applyAxisAngle(new THREE.Vector3(0, 1, 0), d * 0.08);
camera.position.copy(controls.target).add(off);
}
controls.update();
renderer.render(scene, camera);
}
animate();The Full Code
Save as index.html and open it through any local web server (YouTube embeds need a real http origin, not file://), or hit Launch. Copy and Launch use the same free sign-in as the live demo.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>3D YouTube Video Cube: Three.js CSS3DRenderer</title>
<style>
html, body { margin: 0; height: 100%; height: 100dvh; overflow: hidden; background: radial-gradient(ellipse at 50% 35%, #0b1a2e 0%, #03060c 70%); font-family: Arial, sans-serif; color: #e2e8f0; }
#stage { position: fixed; inset: 0; touch-action: none; }
.screen { width: 480px; height: 270px; background: #000; border: 3px solid #2ad1d6; box-shadow: 0 0 40px rgba(42,209,214,.55); }
.screen iframe { width: 100%; height: 100%; border: 0; display: block; }
/* While the camera is being dragged, the iframes stop catching the pointer. */
body.dragging .screen iframe { pointer-events: none; }
#ui { position: fixed; left: 50%; bottom: calc(14px + env(safe-area-inset-bottom, 0px)); transform: translateX(-50%); display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: rgba(10,14,22,.85); border: 1px solid rgba(42,209,214,.25); border-radius: 12px; z-index: 5; }
#ui button { background: #13202e; color: #2ad1d6; border: 1px solid rgba(42,209,214,.4); border-radius: 8px; min-width: 34px; padding: 6px 10px; font-size: 13px; cursor: pointer; }
#ui button.on { background: #2ad1d6; color: #06252a; font-weight: 700; }
#ui label { font-size: 12px; color: #93a4b8; display: flex; align-items: center; gap: 4px; }
@media (max-height: 500px) and (orientation: landscape) { #ui { padding: 4px 8px; bottom: calc(6px + env(safe-area-inset-bottom, 0px)); } #ui button { padding: 4px 8px; font-size: 11px; } }
</style>
<script type="importmap">
{ "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" } }
</script>
</head>
<body>
<div id="stage"></div>
<div id="ui"></div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';
const VIDEOS = [
['67Ii__vPouE', 'First person shooter fire fountain'],
['_n8Yb_j5E2o', 'Interactive particle programming'],
['STRk1CWK8JQ', 'Space X launch terrain'],
['mhuxhXU9PPY', 'Robot battle game'],
];
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 1, 5000);
// Pull back on narrow (portrait) screens so the whole cube fits.
camera.position.set(0, 120, Math.max(1050, 1300 / camera.aspect));
const renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, innerHeight);
document.getElementById('stage').appendChild(renderer.domElement);
// One CSS3DObject per side: a real YouTube iframe moved into 3D with CSS transforms.
// Each screen is 480 wide, so it sits 240 out from the centre.
const R = 240;
function screen(id, angle) {
const div = document.createElement('div');
div.className = 'screen';
const iframe = document.createElement('iframe');
iframe.src = 'https://www.youtube.com/embed/' + id + '?rel=0&playsinline=1';
iframe.allow = 'autoplay; encrypted-media; picture-in-picture';
div.appendChild(iframe);
const obj = new CSS3DObject(div);
obj.position.set(Math.sin(angle) * R, 0, Math.cos(angle) * R);
obj.rotation.y = angle;
return obj;
}
const cube = new THREE.Group();
VIDEOS.forEach(([id], i) => cube.add(screen(id, i * Math.PI / 2)));
scene.add(cube);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.autoRotate = true; controls.autoRotateSpeed = 1.2;
controls.minDistance = 600; controls.maxDistance = 3200;
// Stop the iframes catching the mouse or finger while orbiting, then hand it back.
controls.addEventListener('start', () => document.body.classList.add('dragging'));
controls.addEventListener('end', () => document.body.classList.remove('dragging'));
// Buttons 1-4 swing the camera round to face that video and stop the spin.
const ui = document.getElementById('ui');
let target = null;
VIDEOS.forEach(([, name], i) => {
const b = document.createElement('button');
b.textContent = i + 1; b.title = name; b.setAttribute('aria-label', 'Face ' + name);
b.onclick = () => {
target = i * Math.PI / 2; controls.autoRotate = false; spin.checked = false;
ui.querySelectorAll('button').forEach((x) => x.classList.toggle('on', x === b));
};
ui.appendChild(b);
});
const lab = document.createElement('label');
lab.innerHTML = '<input type="checkbox" checked> Spin';
const spin = lab.querySelector('input');
spin.onchange = () => { controls.autoRotate = spin.checked; if (spin.checked) target = null; };
ui.appendChild(lab);
function animate() {
requestAnimationFrame(animate);
if (document.hidden) return;
if (target !== null) {
// Ease the camera round the Y axis toward the chosen side.
const off = camera.position.clone().sub(controls.target);
let d = target - Math.atan2(off.x, off.z); d = Math.atan2(Math.sin(d), Math.cos(d));
if (Math.abs(d) < 0.002) target = null;
off.applyAxisAngle(new THREE.Vector3(0, 1, 0), d * 0.08);
camera.position.copy(controls.target).add(off);
}
controls.update();
renderer.render(scene, camera);
}
animate();
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>
Taking It Further
- Mix CSS3D with a WebGL scene, curved screens and a mirror floor: YouTube Video in Three.js
- Control the players with JavaScript in the YouTube IFrame API tutorial
- Watch the gameplay, then play it: Super Soldier Battle