What WebGL is, in plain English
WebGL (Web Graphics Library) is a JavaScript API that talks directly to your GPU via the
<canvas> element. It's based on OpenGL ES — the same
graphics standard that powers mobile games and AR apps — and it runs in every modern browser
with no plugins.
Where 2D Canvas stops (a few thousand shapes before it chokes), WebGL keeps going (millions of polygons at 60fps). The cost is complexity: raw WebGL is verbose, so most people use a library like Three.js on top.
When to use WebGL (and when not to)
Use WebGL for
- 3D product viewers — rotate a shoe, inspect a car, customize a sofa
- Data visualization at scale — 100k+ points, globes, flow maps
- Games & interactive experiences — browser-based 3D games, simulations
- Shader effects — fluid sims, noise, post-processing, generative art
- Immersive marketing — hero backgrounds, scroll-driven storytelling
- AR / VR on the web — WebXR sits on top of WebGL
Don't use WebGL for
- Regular UI — HTML/CSS is way faster to build & more accessible
- Text-heavy content — WebGL text is hard to make accessible or selectable
- Simple 2D charts — use SVG or 2D Canvas; WebGL is overkill
- Form elements — you'd be rebuilding the browser
- SEO-critical content — crawlers can't read pixels on a canvas
- Battery-sensitive flows — the GPU is a power hog on mobile
Real-world WebGL in the wild
Sites you've probably used are powered by WebGL. Here's the range of what's possible:
Product configurators
Nike By You, Tesla design studio, IKEA room planner — rotate, recolor, customize in real time.
Maps & globes
Google Earth, Mapbox GL, deck.gl — rendering millions of map tiles & data points.
Browser games
Agar.io, Slither, browser ports of classic 3D titles — no download required.
Big data viz
Observable, Kepler.gl, Plotly — visualizing hundreds of thousands of records smoothly.
Creative / portfolios
Awwwards winners, Bruno Simon, Active Theory — scroll-triggered 3D storytelling.
WebXR
VR meetings, AR try-on, 360° tours — all run on WebGL under the hood.
Raw WebGL — the hello-world triangle
Drawing one triangle takes ~40 lines of code in raw WebGL. You write two small programs (shaders), upload vertex data to the GPU, and tell it to draw. It's a lot for a triangle — but it shows you everything a library like Three.js abstracts away.
<canvas id="c" width="520" height="260"></canvas>
const canvas = document.getElementById('c');
const gl = canvas.getContext('webgl');
// 1. Vertex shader — runs once per vertex
const vSrc = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
// 2. Fragment shader — runs once per pixel
const fSrc = `
precision mediump float;
void main() {
gl_FragColor = vec4(0.52, 0.80, 0.09, 1.0); // lime green
}
`;
function compile(type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
return s;
}
// 3. Link shaders into a program
const program = gl.createProgram();
gl.attachShader(program, compile(gl.VERTEX_SHADER, vSrc));
gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fSrc));
gl.linkProgram(program);
gl.useProgram(program);
// 4. Upload triangle vertices
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.7,
-0.7, -0.5,
0.7, -0.5
]), gl.STATIC_DRAW);
// 5. Tell the program how to read the buffer
const loc = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
// 6. Draw!
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);Three.js — a rotating 3D cube in 30 lines
Three.js is the most popular WebGL library. Scene, camera, mesh, renderer — four concepts get you a real 3D scene with lighting, materials, and a 60fps animation loop.
import * as THREE from 'three';
const stage = document.getElementById('cubeStage');
// 1. Scene — the container for everything
const scene = new THREE.Scene();
// 2. Camera — what the viewer sees
const camera = new THREE.PerspectiveCamera(
60, stage.clientWidth / stage.clientHeight, 0.1, 100
);
camera.position.z = 3;
// 3. Renderer — pushes pixels to the canvas
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(stage.clientWidth, stage.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
stage.appendChild(renderer.domElement);
// 4. A lit cube
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1.2, 1.2, 1.2),
new THREE.MeshStandardMaterial({ color: 0x84cc16, metalness: .3, roughness: .4 })
);
scene.add(cube);
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const key = new THREE.DirectionalLight(0xffffff, 1.2);
key.position.set(3, 4, 5);
scene.add(key);
// 5. Animation loop
function frame() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.012;
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
frame();<!-- Use an import map so 'three' resolves from the CDN -->
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js"
}
}
</script>
<div id="cubeStage" style="width:100%; height:400px;"></div>
<script type="module" src="cube.js"></script>Three.js — 3,000 particles that follow your mouse
This is where WebGL earns its keep: rendering thousands of objects smoothly. Move your mouse over the stage — each particle gets pushed based on cursor position, and the GPU handles every pixel.
import * as THREE from 'three';
const stage = document.getElementById('particleStage');
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
60, stage.clientWidth / stage.clientHeight, 0.1, 100
);
camera.position.z = 4;
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(stage.clientWidth, stage.clientHeight);
stage.appendChild(renderer.domElement);
// Build 3,000 random particle positions
const COUNT = 3000;
const positions = new Float32Array(COUNT * 3);
for (let i = 0; i < COUNT; i++) {
positions[i*3+0] = (Math.random() - 0.5) * 6;
positions[i*3+1] = (Math.random() - 0.5) * 3;
positions[i*3+2] = (Math.random() - 0.5) * 3;
}
const geom = new THREE.BufferGeometry();
geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color: 0x84cc16, size: 0.03, transparent: true, opacity: 0.9
});
const points = new THREE.Points(geom, mat);
scene.add(points);
// Track mouse
const mouse = { x: 0, y: 0 };
stage.addEventListener('pointermove', e => {
const r = stage.getBoundingClientRect();
mouse.x = ((e.clientX - r.left) / r.width) * 2 - 1;
mouse.y = -((e.clientY - r.top) / r.height) * 2 + 1;
});
function frame() {
points.rotation.y += 0.002;
points.rotation.x += (mouse.y * 0.3 - points.rotation.x) * 0.05;
points.rotation.z += (mouse.x * 0.3 - points.rotation.z) * 0.05;
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
frame();Custom shader — animated gradient noise
Shaders are programs that run per-pixel on the GPU. They're how every visual effect in modern games is built. Here's one that generates a flowing gradient from scratch — no textures, no images, just math.
// Fragment shader — runs for every pixel, every frame
precision mediump float;
uniform vec2 u_res;
uniform float u_time;
void main() {
vec2 uv = gl_FragCoord.xy / u_res;
// Two moving sine waves mixed together
float a = sin(uv.x * 8.0 + u_time * 0.7);
float b = sin(uv.y * 6.0 + u_time * 0.9);
float mixv = 0.5 + 0.5 * (a * b);
// Blend between green and teal
vec3 col = mix(
vec3(0.52, 0.80, 0.09),
vec3(0.08, 0.72, 0.65),
mixv
);
gl_FragColor = vec4(col, 1.0);
}// Full-screen triangle + fragment shader → animated gradient.
// This is how Shadertoy, gradient.io, and every "shader toy"
// demo on the web is built.
const gl = canvas.getContext('webgl');
// Compile & link the shader program (see Example 1)...
// Every frame, update time + redraw
function frame(t) {
gl.uniform1f(timeLoc, t * 0.001);
gl.uniform2f(resLoc, canvas.width, canvas.height);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);Which library should you pick?
Unless you're writing your own engine, start with a library. Here are the four worth knowing:
Three.js
The de-facto WebGL library. Huge ecosystem, great docs, endless examples. Start here.
Babylon.js
Microsoft-backed game engine for the web. Heavier than Three, with built-in physics and a visual editor.
PixiJS
2D-focused — uses WebGL under the hood for blazing-fast 2D rendering. Great for games, ads, UI effects.
Raw WebGL
Maximum control, maximum effort. Only worth it if you're building an engine or need a tiny footprint.
Quick-win performance tips
- Check
caniuse.com/webgl— it's supported everywhere, but mobile GPUs vary wildly in power. - Use instanced meshes to draw thousands of copies with one draw call.
- Cap your
pixelRatioat 2 — drawing at 3×DPI on a retina iPhone doubles your fragment-shader work. - Prefer
BufferGeometryover the legacyGeometry(Three.js handles this for you in v125+). - Kill the render loop when the canvas scrolls out of view — use
IntersectionObserver. - Offer a 2D fallback for users with WebGL disabled or on low-end devices.