Introduction to WebGL shaders

June 16, 2025

WebGL shaders are a powerful instrument that enables us to do things that are not possible on the native web. When I started coding in JavaScript, I randomly came across the Bruno Simon portfolio, and it blew my mind. If you’ve ever wondered how to implement those 3D interactions, you've come to the right place. Now, we'll dive into WebGL shaders and how to apply them in JavaScript.

What are WebGL Shaders?

Firstly, let's understand what we are dealing with.

WebGL, short for Web Graphics Library, is a JavaScript API that allows you to render 3D graphics in a web browser. Shaders are small programs that run on your GPU and determine the final look of your graphics.

There are two main types of shaders:

  • Vertex Shaders: Processes each vertex's attributes (position, color, texture) in 3D models, transforming them to screen coordinates in the rendering pipeline.
  • Fragment Shaders: Calculates the color and other attributes of each pixel, determining the final appearance of surfaces in a rendered image.

In this article, we will focus on the fragment shaders, which is where most of the visual magic happens.

WebGL

Setting up the project

We'll start with a simple HTML file. Create an index.html file with the following structure:

1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta http-equiv="X-UA-Compatible" content="ie=edge" />
6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 <title>Dive Into WebGL Shaders</title>
8<style>
9 body, html {
10 margin: 0;
11 overflow: hidden;
12 height: 100%;
13 }
14 canvas {
15 display: block;
16 }
17</style>
18</head>
19 <body>
20 <canvas id="glcanvas"></canvas>
21 <script src="main.js"></script>
22 </body>
23</html>

This sets up a full-screen canvas where we will render our shader.

Adding the JavaScript

Now, let's create a main.js file. This will contain the code to initialize WebGL, compile the shader, and render it to the canvas. Let's break it down step by step.

First, we need to get a WebGL rendering context from the canvas.

1async function main() {
2 const canvas = document.getElementById("glcanvas");
3 let gl = canvas.getContext("webgl");
4 if (!gl) {
5 console.error("WebGL not supported, falling back on experimental-webgl");
6 gl = canvas.getContext("experimental-webgl");
7 }
8
9 if (!gl) {
10 alert("Your browser does not support WebGL");
11 return;
12 }
13 // Further steps will go here
14 // ...
15}
16window.onload = main;

We create a reference to our canvas element and attempt to get a WebGL rendering context. This context is necessary for all WebGL operations, as it allows us to call WebGL functions to set up shaders, buffers, and draw operations. We use getContext("webgl") and fall back to experimental-webgl if necessary. If neither context is available, we alert the user.

Fetch and compile shaders

Next, we'll compile the shader code.

Create a new file called shader.glsl and insert this code for a fragment shader I found on shadertoy.com, credits to Danilo Guanabara:

1async function fetchShader(url) {
2 const response = await fetch(url);
3 return await response.text();
4}
5
6async function main() {
7 // ...
8 // ^^^ Initialization (previous step)
9
10 const vertexShaderSource = `
11 attribute vec4 aVertexPosition;
12 void main() {
13 gl_Position = aVertexPosition;
14 }
15 `;
16
17 const fragmentShaderSource = await fetchShader('shader.glsl');
18
19 const vertexShader = gl.createShader(gl.VERTEX_SHADER);
20 gl.shaderSource(vertexShader, vertexShaderSource);
21 gl.compileShader(vertexShader);
22 if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
23 console.error("ERROR compiling vertex shader!", gl.getShaderInfoLog(vertexShader));
24 return;
25 }
26
27 const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
28 gl.shaderSource(fragmentShader, fragmentShaderSource);
29 gl.compileShader(fragmentShader);
30 if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
31 console.error("ERROR compiling fragment shader!", gl.getShaderInfoLog(fragmentShader));
32 return;
33 }
34 // Further steps
35 // ...
36}
37window.onload = main;

We fetch the fragment shader code from an external file and compile both vertex and fragment shaders. They need to be compiled from source code into a form the GPU can execute. The vertex shader positions vertices and the fragment shader determines pixel colors. We use fetch to load the shader code and WebGL functions createShader, shaderSource, and compileShader to compile it. Errors are logged if compilation fails.

Link shaders into a program

With our shaders compiled, we need to link them into a WebGL program.

1async function main() {
2 // ...
3 // ^^^ Initialization and shader compilation
4
5 const shaderProgram = gl.createProgram();
6 gl.attachShader(shaderProgram, vertexShader);
7 gl.attachShader(shaderProgram, fragmentShader);
8 gl.linkProgram(shaderProgram);
9 if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
10 console.error("ERROR linking program!", gl.getProgramInfoLog(shaderProgram));
11 return;
12 }
13 gl.useProgram(shaderProgram);
14 // Further steps
15 // ...
16}
17window.onload = main;

We link the compiled vertex and fragment shaders into a single program. Linking creates a complete GPU program that combines the vertex and fragment shaders, allowing them to work together. We use createProgram, attachShader, and linkProgram to link the shaders. If linking fails, an error is logged.

Create and bind Buffers

We need to set up a buffer with vertex positions and instruct WebGL to draw the scene.

1async function main() {
2 // ...
3 // ^^^ Initialization, shader compilation, and linking
4
5 const vertices = new Float32Array([
6 -1.0, -1.0,
7 1.0, -1.0,
8 -1.0, 1.0,
9 -1.0, 1.0,
10 1.0, -1.0,
11 1.0, 1.0
12 ]);
13
14 const positionBuffer = gl.createBuffer();
15 gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
16 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
17
18 const positionAttributeLocation = gl.getAttribLocation(shaderProgram, "aVertexPosition");
19 gl.enableVertexAttribArray(positionAttributeLocation);
20 gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
21 // Further steps
22 // ...
23}
24window.onload = main;

We create a buffer to hold the vertex positions and bind it to the attribute in our vertex shader. Buffers store vertex data that the GPU uses to render shapes. We need to pass this data to the vertex shader. We use createBuffer, bindBuffer, and bufferData to create and fill the buffer. We then use getAttribLocation, enableVertexAttribArray, and vertexAttribPointer to bind the buffer to the shader attribute.

Set Uniforms and Render

Finally, let's write the rendering loop. This will update the time uniform and draw the shader to the screen.

1async function main() {
2 // ...
3 // ^^^ Initialization, shader compilation, linking, and buffer setup
4
5 const resolutionUniformLocation = gl.getUniformLocation(shaderProgram, "iResolution");
6 const timeUniformLocation = gl.getUniformLocation(shaderProgram, "iTime");
7
8 function render(time) {
9 gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height);
10 gl.uniform1f(timeUniformLocation, time * 0.001);
11 gl.drawArrays(gl.TRIANGLES, 0, 6);
12 requestAnimationFrame(render);
13 }
14
15 function resizeCanvas() {
16 canvas.width = window.innerWidth;
17 canvas.height = window.innerHeight;
18 gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
19 }
20
21 window.addEventListener('resize', resizeCanvas);
22 resizeCanvas();
23 requestAnimationFrame(render);
24}
25window.onload = main;

We set the values for the uniform variables iResolution and iTime and start the rendering loop. Uniforms provide data that is constant across a single draw call. We use them to pass the canvas size and current time to the shader. We use getUniformLocation, uniform2f, and uniform1f to set the uniform values. The render function updates these values and draws the scene. The resizeCanvas function ensures the canvas size matches the window size, and requestAnimationFrame keeps the render loop running.

Bringing It All Together

Your main.js file should look like this:

1async function fetchShader(url) {
2 const response = await fetch(url);
3 return await response.text();
4}
5
6async function main() {
7 const canvas = document.getElementById("glcanvas");
8 let gl = canvas.getContext("webgl");
9 if (!gl) {
10 console.error("WebGL not supported, falling back on experimental-webgl");
11 gl = canvas.getContext("experimental-webgl");
12 }
13
14 if (!gl) {
15 alert("Your browser does not support WebGL");
16 return;
17 }
18
19 const vertexShaderSource = `
20 attribute vec4 aVertexPosition;
21 void main() {
22 gl_Position = aVertexPosition;
23 }
24 `;
25
26 const fragmentShaderSource = await fetchShader('shader.glsl');
27
28 const vertexShader = gl.createShader(gl.VERTEX_SHADER);
29 gl.shaderSource(vertexShader, vertexShaderSource);
30 gl.compileShader(vertexShader);
31 if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
32 console.error("ERROR compiling vertex shader!", gl.getShaderInfoLog(vertexShader));
33 return;
34 }
35
36 const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
37 gl.shaderSource(fragmentShader, fragmentShaderSource);
38 gl.compileShader(fragmentShader);
39 if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
40 console.error("ERROR compiling fragment shader!", gl.getShaderInfoLog(fragmentShader));
41 return;
42 }
43
44 const shaderProgram = gl.createProgram();
45 gl.attachShader(shaderProgram, vertexShader);
46 gl.attachShader(shaderProgram, fragmentShader);
47 gl.linkProgram(shaderProgram);
48 if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
49 console.error("ERROR linking program!", gl.getProgramInfoLog(shaderProgram));
50 return;
51 }
52
53 gl.useProgram(shaderProgram);
54
55 const vertices = new Float32Array([
56 -1.0, -1.0,
57 1.0, -1.0,
58 -1.0, 1.0,
59 -1.0, 1.0,
60 1.0, -1.0,
61 1.0, 1.0
62 ]);
63
64 const positionBuffer = gl.createBuffer();
65 gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
66 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
67
68 const positionAttributeLocation = gl.getAttribLocation(shaderProgram, "aVertexPosition");
69 gl.enableVertexAttribArray(positionAttributeLocation);
70 gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
71
72 const resolutionUniformLocation = gl.getUniformLocation(shaderProgram, "iResolution");
73 const timeUniformLocation = gl.getUniformLocation(shaderProgram, "iTime");
74
75 function render(time) {
76 gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height);
77 gl.uniform1f(timeUniformLocation, time * 0.001);
78 gl.drawArrays(gl.TRIANGLES, 0, 6);
79 requestAnimationFrame(render);
80 }
81
82 function resizeCanvas() {
83 canvas.width = window.innerWidth;
84 canvas.height = window.innerHeight;
85 gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
86 }
87
88 window.addEventListener('resize', resizeCanvas);
89 resizeCanvas();
90 requestAnimationFrame(render);
91}
92window.onload = main;

Result

If you followed all these steps, you can now open your index.html file in the browser and you should be able to see this wonderful animated shader rendered in a canvas element:

Shader

Facts, Tips & Conclusions

  • WebGL was developed by the Khronos Group, the same folks behind OpenGL.
  • Shaders can be extremely powerful, but they also come with performance caveats. Overusing complex shaders can slow down your application, especially on lower-end devices.
  • WebGL 2.0 offers enhanced features like multiple render targets and 3D textures compared to its predecessor, WebGL 1.0.
  • Always optimize your shaders by minimizing calculations inside loops and avoiding unnecessary operations.
  • Debugging shaders can be tricky. Tools like WebGL Inspector and Chrome’s built-in WebGL debugging can be lifesavers.
  • MDN WebGL API documentation
  • MDN GLSL shaders documentation
  • Check if your browser supports WebGL
  • WebGL fundamentals course
  • Awwwards inspiration websites using WebGL
  • WebGL 3D Graphics Explained in 100 Seconds

Here is a Codesandbox with the full working code. Happy hacking!

Utopia Background
Travis Scott

Travis Scott

30M monthly listeners

UTOPIA

MY EYES

UTOPIA

Kyiv

30 °

Air quality alert

H: 30°

L: 16°

16

30°

17

30°

18

30°

19

29°

20

29°

21:12

27°