Three.js From Zero · Article s14-04
Three.js + Web Components
Three.js + Web Components is Article s14-04 of Three.js From Zero, a MasterAllArts free interactive lesson for artists learning creative 3D on the web.
Season 14 · Article 04 · Setup & Tooling
Wrap a Three.js scene in a Custom Element. Drop it into any site — Webflow, Notion (some embeds), Framer, Squarespace, plain HTML. No React, no build step, no framework lock-in. The most portable 3D you'll ever ship.
The minimum custom element
class ThreeScene extends HTMLElement {
connectedCallback() {
const w = this.clientWidth || 400;
const h = this.clientHeight || 300;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, w/h, 0.1, 50);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(w, h);
this.appendChild(renderer.domElement);
const cube = new THREE.Mesh(
new THREE.BoxGeometry(),
new THREE.MeshNormalMaterial(),
);
scene.add(cube);
renderer.setAnimationLoop((t) => {
cube.rotation.x = cube.rotation.y = t * 0.001;
renderer.render(scene, camera);
});
this._renderer = renderer;
this._scene = scene;
}
disconnectedCallback() {
this._renderer.dispose();
this._scene.traverse((o) => { if (o.geometry) o.geometry.dispose(); });
}
}
customElements.define('three-scene', ThreeScene);
Now anywhere on any page: <three-scene style="width:400px;height:300px"></three-scene> and it works.
Attributes as props
static get observedAttributes() { return ['color', 'shape']; }
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'color' && this._cube) {
this._cube.material.color.set(newValue);
}
}
Now the parent can pass props via HTML attributes:
<three-scene color="#ec4899" shape="torus"></three-scene>
<script>
// Or change dynamically
document.querySelector('three-scene').setAttribute('color', '#22d3ee');
</script>
Shadow DOM to isolate styles
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
// ...
this.shadowRoot.appendChild(renderer.domElement);
}
Shadow DOM prevents the host page's CSS from affecting your canvas (and vice versa). For embeddable widgets, this is essential — Webflow has a million global styles that will break your canvas without isolation.
Bundling for embed
// vite.config — library mode for distribution
export default {
build: {
lib: {
entry: 'src/three-scene.ts',
name: 'ThreeScene',
formats: ['iife'], // single <script> tag
fileName: 'three-scene',
},
},
};
Produces three-scene.iife.js — a single file users drop in their site with <script src="..."></script>. Three.js bundled in. Total size ~150KB gzip for a typical scene.
Loading models from a URL attribute
<three-scene src="https://yoursite.com/model.glb"></three-scene>
attributeChangedCallback(name, _, newValue) {
if (name === 'src') {
new GLTFLoader().load(newValue, (gltf) => this._scene.add(gltf.scene));
}
}
Common first-time pitfalls
:host { display: block; } inside a <style> in the shadow root. Or set width/height attributes.this.renderer.domElement. By default the wheel propagates to the page (parent scrolls).Exercises
- Build <product-viewer>. Takes
srcattribute for a glTF URL. Supports OrbitControls. Add a "fit to view" method via auto-bounding-box. Self-contained product viewer for any e-commerce site. - Slot for HTML content. Add a default slot in shadow DOM. Users can pass HTML overlays:
<three-scene><p>Hover the cube</p></three-scene>. - Ship to npm. Publish your component. Users install:
npm i your-three-sceneandimport 'your-three-scene'. Element is auto-registered.
UP NEXT
S14-05 — Deploying Three.js Apps → Vercel, CF Pages, GH Pages, cache headers.