Tutorial

What WebGL is — and when to use it

WebGL turns your browser into a GPU-powered renderer. Ship 3D scenes, shaders, particles, and data viz that would melt any 2D canvas. Start here.

Step 1

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.

Mental model: You send vertex positions + shader programs to the GPU. The GPU runs those shaders in parallel across thousands of cores and draws the result into the canvas. Every frame. At 60fps.
Step 2

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
Step 3

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.

Example 1

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.

Raw WebGL · 40 lines
<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);
Takeaway: This is one triangle. A cube is 12 triangles. A sphere is hundreds. Handling that by hand gets painful fast — which is why almost nobody writes raw WebGL for production. Read on.
Example 2

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.

Three.js · 30 lines
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>
Example 3

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.

3,000 particles · 60fps
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();
Example 4

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.

GLSL fragment shader
// 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);
Learn shaders: Shadertoy.com is the playground. The Book of Shaders (thebookofshaders.com) is the textbook. Both free.
Step 4

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.

3DBeginner-friendlyMost popular

Babylon.js

Microsoft-backed game engine for the web. Heavier than Three, with built-in physics and a visual editor.

GamesFull engineTypeScript

PixiJS

2D-focused — uses WebGL under the hood for blazing-fast 2D rendering. Great for games, ads, UI effects.

2DGamesAnimation

Raw WebGL

Maximum control, maximum effort. Only worth it if you're building an engine or need a tiny footprint.

HardcoreLearn-by-doing
Cheat Sheet

Quick-win performance tips

Next step: Browse threejs.org/examples — every demo has viewable source. The fastest way to learn is to copy one that looks like what you want and tweak it.