Skip to content

Browser API · GLSL

WebGL

The baseline. Everything on this site runs on it.

The reference scene

Live · running on WebGL in your browser right now

What it is

WebGL is OpenGL ES 2.0 (or 3.0, for WebGL2) exposed to JavaScript. It has shipped in every browser for over a decade, works on effectively every device you will meet, and needs no library, no build step and no permission prompt.

It is also the most verbose option here by a wide margin. Drawing one triangle means compiling two shaders, linking a program, creating a buffer, describing its layout, and binding it — before anything appears. That verbosity is why the labs on this site use it: the ceremony is the subject.

Reach for it when

  • You want to understand what the GPU is actually being told, without a layer editorialising.
  • You need to run on anything, including older mobile and locked-down corporate machines.
  • You want zero dependencies and a bundle measured in the code you wrote.

Look elsewhere when

  • You want a scene with models, lights and cameras — you would be rebuilding Three.js badly.
  • You need compute shaders, storage buffers or modern GPU features. That is WebGPU territory.

The code

The same plasma, written the way WebGL wants it written.

The fragment shaderGLSL10 lines
precision highp float;

varying vec2 vUv;
uniform float uTime;

void main() {
  // Inigo Quilez's cosine palette: three cosines a phase apart.
  vec3 colour = 0.5 + 0.5 * cos(uTime + vUv.xyx + vec3(0.0, 2.0, 4.0));
  gl_FragColor = vec4(colour, 1.0);
}
The JavaScript around itJavaScript25 lines
const gl = canvas.getContext('webgl');

const program = createProgram(gl, vertexSource, fragmentSource);
const quad = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quad);
gl.bufferData(
  gl.ARRAY_BUFFER,
  new Float32Array([-1, -1, 3, -1, -1, 3]),
  gl.STATIC_DRAW,
);

const position = gl.getAttribLocation(program, 'aPosition');
const uTime = gl.getUniformLocation(program, 'uTime');

function frame(now) {
  gl.viewport(0, 0, canvas.width, canvas.height);
  gl.useProgram(program);
  gl.bindBuffer(gl.ARRAY_BUFFER, quad);
  gl.enableVertexAttribArray(position);
  gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
  gl.uniform1f(uTime, now / 1000);
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

Two shaders compiled, a program linked, one quad uploaded, one uniform updated per frame. This is the whole thing.

A lit, spinning cube

Twelve triangles, three vertex attributes, an index buffer, a model and a view-projection matrix, one directional light, backface culling and a depth buffer. The picture is identical in all four; the amount you have to write to get it is not.

~118 lines here, against 25 for the plasma

Live · running on WebGL in your browser right now

The plumbingTypeScript21 lines
// Three attribute buffers, described one call at a time.
for (const [name, buffer] of [
  ['aPosition', positions],
  ['aNormal', normals],
  ['aColor', colors],
] as const) {
  const location = gl.getAttribLocation(program, name);
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.enableVertexAttribArray(location);
  gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0);
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indices);

// Depth and culling are global switches, flipped per draw.
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.enable(gl.CULL_FACE);

gl.uniformMatrix4fv(uModel, false, model);
gl.uniformMatrix4fv(uViewProjection, false, viewProjection);
gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, 0);

Every piece of state here is global and sticky: whatever was enabled last is still enabled. That is the whole difference from WebGPU, which freezes the same information into a pipeline object up front.

Things that will catch you

One big triangle, not a quad
A full-screen effect is usually drawn as a single oversized triangle rather than two triangles forming a square. It covers the same pixels with one fewer primitive and no seam down the diagonal where the two triangles meet.
Context loss is real and you must handle it
The browser can take your GPU context away at any moment — tab backgrounded, driver reset, another tab being greedy. Listen for webglcontextlost or your canvas silently freezes.
gl.lineWidth mostly does nothing
Almost every implementation clamps it to 1. If you need thick lines you build them from triangles. This surprises everyone exactly once.

Where to learn it

These and 33 more on the reading path.