WebGPU icon shader
The icon at the top of this page is a live WebGPU fragment shader — 17 horizontal rows, each sweeping a warm gradient at a slightly different phase. One uniform buffer, no textures. Falls back to a CSS gradient on browsers without WebGPU.
Sizes
16
24
32
40
48
64
80
96
Colour variants
How it works
The fragment shader divides UV space into rows. Each row computes a phase from time + rowIndex × 0.025, runs it through an eased ping-pong, and slides the gradient x-position via fract(uv.x + offset). Film grain is added on top.
One Uniforms struct holds resolution and time — written each frame via writeBuffer. No vertex data, no textures — just a full-screen triangle and math.
icon.wgsl
struct Uniforms { resolution: vec2f, time: f32, _pad: f32, color0: vec4f, color1: vec4f, color2: vec4f, color3: vec4f, }; @group(0) @binding(0) var<uniform> uniforms: Uniforms; struct VertexOutput { @builtin(position) position: vec4f, @location(0) uv: vec2f, }; @vertex fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { let pos = array( vec2f(-1.0, 3.0), vec2f(-1.0, -1.0), vec2f( 3.0, -1.0) ); let uvs = array( vec2f(0.0, 0.0), vec2f(0.0, 2.0), vec2f(2.0, 2.0) ); var output: VertexOutput; output.position = vec4f(pos[vertexIndex], 0.0, 1.0); output.uv = uvs[vertexIndex]; return output; } fn rand(co: vec2f) -> f32 { return fract(sin(dot(co, vec2f(12.9898, 78.233))) * 43758.5453); } fn grain(uv: vec2f, strength: f32) -> f32 { return (rand(uv * 1000.0) - 0.5) * strength; } fn easeInOutCubic(t: f32) -> f32 { if (t < 0.5) { return 4.0 * t * t * t; } return 1.0 - pow(-2.0 * t + 2.0, 3.0) / 2.0; } fn easedPingPong(t: f32) -> f32 { let pingPong = abs(fract(t) * 2.0 - 1.0); return easeInOutCubic(pingPong); } @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4f { let fragCoord = input.position.xy; let uv = input.uv; let time = uniforms.time; let c0 = uniforms.color0.rgb; let c1 = uniforms.color1.rgb; let c2 = uniforms.color2.rgb; let c3 = uniforms.color3.rgb; let numRows = 17.0; let rowIndex = floor(uv.y * numRows); let a2 = sin(time * 0.001 + rowIndex * 0.0001); let baseOffset = rowIndex * a2; let animSpeed = 0.05; let phase = time * animSpeed + rowIndex * 0.025; let eased = easedPingPong(phase); let animOffset = eased * 1.0; let gradX = fract(uv.x + baseOffset + animOffset); let colors = array<vec3f, 5>(c0, c1, c2, c3, c0); let t = gradX * 3.0; let segment = u32(t); let f = fract(t); var color = mix(colors[segment], colors[segment + 1], f); // grain let normalizedCoord = fragCoord / uniforms.resolution; let noiseVal = grain(normalizedCoord * 10.0, 0.06); color = color + vec3f(noiseVal); return vec4f(color, 1.0); }