Gradient shader
Table of contents
Brought to you by:<br>Wayne Enterprises - The future of Gotham
Gradient shader<br>Gradient shader<br>Planted: May 2026<br>Status: seed<br>Planted: May 2026<br>Intended Audience: Creative coders and front-end developers with a basic understanding of WebGL shaders.
How to create an organic gradient animation using a WebGL shader. If you're new to shaders, check out this note. What we'll make:
Render shader<br>First we render a simple shader — a flat green surface.
This requires:<br>▪ a canvas element in the DOM<br>▪ vertex and fragment shader files<br>▪ a program to compile these files<br>▪ a render loop<br>canvas id="canvas">canvas>
import vertSrc from "./shader.vert?raw";<br>import fragSrc from "./shader.frag?raw";
const canvas = document.querySelector("#canvas");<br>const gl = canvas.getContext("webgl2");
function compileShader(type, src) {<br>const shader = gl.createShader(type);<br>gl.shaderSource(shader, src);<br>gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {<br>throw new Error(gl.getShaderInfoLog(shader));
return shader;
// A program links a vertex shader and fragment shader into one GPU pipeline.<br>const program = gl.createProgram();<br>gl.attachShader(program, compileShader(gl.VERTEX_SHADER, vertSrc));<br>gl.attachShader(program, compileShader(gl.FRAGMENT_SHADER, fragSrc));<br>gl.linkProgram(program);
// WebGL2 requires a bound VAO to draw.<br>gl.bindVertexArray(gl.createVertexArray());
function render() {<br>gl.clearColor(0, 0, 0, 1);<br>gl.clear(gl.COLOR_BUFFER_BIT);<br>gl.useProgram(program);
// Process 3 vertices (positions hardcoded in vertex shader)<br>gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(render);
requestAnimationFrame(render);
Normally, you'd pass a geometry — like a flat plane — from JavaScript to the vertex shader, then manipulate its vertex positions to create a desired shape.<br>However, we just need a flat surface.<br>So, we use a shortcut.<br>Instead of passing in a geometry, I define three points inside the vertex shader.<br>They create a triangle big enough to cover the viewport.<br>The GPU clips the triangle, leaving a viewport sized rectangle.<br>#version 300 es
void main() {<br>// The vertex shader requires coordinates to be in clip space for its output variable `gl_Position`.<br>// Clip space: [-1, -1] (bottom-left) -> [1, 1] (top-right)<br>vec2 pos[3] = vec2[](<br>vec2(-1.0, -1.0), // bottom-left<br>vec2( 3.0, -1.0), // bottom-right<br>vec2(-1.0, 3.0) // top<br>);
// gl_VertexID is 0, 1, or 2 — coming from gl.drawArrays(TRIANGLES, 0, 3)<br>gl_Position = vec4(pos[gl_VertexID], 0.0, 1.0);
#version 300 es<br>precision highp float;
out vec4 fragColor;
void main() {<br>// green<br>fragColor = vec4(0.0, 1.0, 0.0, 1.0);
smoothstep() + mix()<br>Two built-in functions we'll use are smoothstep and mix.<br>They allow us to create a linear gradient.
I wired some variables from the JS file to a GUI, then passed them into the fragment shader (see sending data for more details).<br>In the shader, I first get the UV — the normalized coordinates of the fragment.<br>The shader provides us with gl_FragCoord, which contains the fragment's coords relative to screen resolution (window-space position).<br>In this coord system, the bottom-left is [0,0] and the top-right might be [1920,1080] (depending on screen size).<br>We normalise these to make things easier to work with.<br>I'm normalizing to [-1, -1] -> [1, 1] because having [0, 0] at the center is ideal for our requirements.<br>Next, I use smoothstep.<br>It takes three numbers — a start, an end and a value in between.<br>It returns a fraction of where that value lies within start to end.<br>For example, it returns:<br>▪ 0 if the value is at the start<br>▪ 1 if at the end<br>▪ and 0.5 if in the middle<br>I set the return value as t then pass it to mix with two colors. If:<br>▪ t == 0, it returns the first color.<br>▪ t == 1, it returns the second color.<br>▪ anything in between will be a mix of the two.<br>#version 300 es<br>precision highp float;
uniform vec2 u_resolution;<br>uniform float u_edge0;<br>uniform float u_edge1;<br>uniform vec3 u_colorA;<br>uniform vec3 u_colorB;
out vec4 fragColor;
vec2 getUV(vec4 fragCoord, vec2 resolution) {<br>vec2 uv = (fragCoord.xy / resolution) * 2.0 - 1.0;<br>// prevent warping because viewport isn't square<br>uv.x *= resolution.x / resolution.y;
return uv;
void main() {<br>vec2 uv = getUV(gl_FragCoord, u_resolution);<br>float t = smoothstep(u_edge0, u_edge1, uv.y);<br>vec3 color = mix(u_colorA, u_colorB, t);
fragColor = vec4(color, 1.0);
Sine wave<br>To create an organic animation, we base the gradient on a sine wave instead of a straight line.
To render the wave:<br>▪ get the UV<br>▪ pass the fragment's x-coord to a sine wave function to get the wave's y-coord<br>▪ call renderWaveLine — a function that uses smoothstep and mix to return:▪ black if this fragment is far from the line,<br>▪ a tiny black to white gradient if close or<br>▪ white if on the line
...<br>out vec4 fragColor;
vec2 getUV(vec4 fragCoord, vec2 resolution) {<br>vec2 uv = (fragCoord.xy / resolution) * 2.0 - 1.0;<br>uv.x *= resolution.x / resolution.y;
return...