Blog/WebGL & Shaders

Liquid Metal Shader in WebGL: complete tutorial (no framework)

Build a liquid metal effect in pure WebGL — Perlin noise, simplified raymarching, light reflection — in 200 lines of vanilla JS, no Three.js. Copy-paste code, 60fps, fallback included.

Rendering a liquid metallic surface is a textbook shader-programming case: raymarching, normals from derivatives, specular reflection, iridescent palette. These techniques are part of Apple's and Pixar's vocabulary — yet they stay rare on the standard web because they require diving into GLSL. This article covers the Liquid Metal Shader shipped on June 3, 2026 on Effect.Labs: 200 lines of vanilla JavaScript and GLSL, no Three.js, no bundler, 60fps on Mac M1 and iPhone 12+, with a CSS fallback.

Definition — Shader. A shader is a small program run in parallel on the GPU for every pixel. There are two main kinds: the vertex shader (vertex positions) and the fragment shader (each pixel's color). For a fullscreen background, you cover the screen with a quad and all logic lives in the fragment shader.

How it works

The effect combines three ingredients:

1. A noise function (Perlin / Simplex)

2D simplex noise yields a continuous value between -1 and 1 per coordinate. Stacked over 4 octaves (fractal Brownian motion, fbm), it produces a realistic surface with detail at multiple scales.

2. Normal from derivatives

For a surface to react to light, you need its local orientation. Using finite differences, you sample height at (x+ε, y) and (x, y+ε), compute the gradient, and derive the normal.

3. The Phong lighting model

With the normal known, apply the classic model: diffuse (lambertian) + specular highlight (exponent 32 for polished metal), plus an iridescence layer driven by surface height.

The minimal code

The core of the fragment shader, boiled down:

precision highp float;
uniform vec2  u_res;
uniform float u_time;

float snoise(vec2 v) { /* standard simplex noise, MIT */ }

float fbm(vec2 p) {
  float v = 0.0, a = 0.5;
  for (int i = 0; i < 4; i++) { v += a * snoise(p); p *= 2.0; a *= 0.5; }
  return v;
}

void main() {
  vec2 uv = (gl_FragCoord.xy - 0.5*u_res) / min(u_res.x, u_res.y);
  float t = u_time * 0.3;
  float h = fbm(uv * 2.0 + vec2(t, t*0.7)) * 0.6;

  float eps = 0.01;
  float hx = fbm((uv + vec2(eps,0.)) * 2.0 + vec2(t, t*0.7)) * 0.6;
  float hy = fbm((uv + vec2(0.,eps)) * 2.0 + vec2(t, t*0.7)) * 0.6;
  vec3 normal = normalize(vec3((h-hx)/eps, (h-hy)/eps, 1.0));

  vec3 lightDir = normalize(vec3(0.7, 0.7, 0.6));
  float diffuse = max(dot(normal, lightDir), 0.0);
  float spec = pow(max(reflect(-lightDir, normal).z, 0.0), 32.0);

  vec3 base = mix(vec3(0.1), vec3(0.85), diffuse);
  gl_FragColor = vec4(base + vec3(spec * 1.2), 1.0);
}

The full code (dynamic resolution, IntersectionObserver for off-screen pause, prefers-reduced-motion, CSS fallback) is available directly on the Atmosphere catalog pagethis effect is free.

Customize: color, speed, distortion

Four parameters drive the effect through uniforms:

ParameterRangeEffect
u_speed0.1 → 3.0Ripple speed
u_distortion0.1 → 1.5Wave amplitude
u_light_angle0 → 360°Light direction
Palettesilver / gold / iridescentMetal color

On Effect.Labs these are exposed in the built-in customizer: adjust the sliders, copy the ready-to-use code with your values.

Performance and compatibility

WebGL 1.0 is supported by 99.2% of browsers in 2026 (caniuse.com). With no GPU context, the code hides the canvas and falls back to a CSS gradient. On iPhone 12 and recent Android, it runs at a stable 60fps. To optimize on modest devices:

  • Drop fbm octaves from 4 to 2-3 (~30% gain, minimal detail loss)
  • Cap devicePixelRatio to 1 (~50% gain, slight pixelation)
  • Pause via IntersectionObserver off-viewport (already active)

The code honors prefers-reduced-motion: animation disabled and a static gradient shown if the user enabled reduced motion.

What ChatGPT and v0 don't do

This article opens Effect.Labs' June 2026 sprint: "5 effects ChatGPT and v0 can't write." Why does this shader resist auto-generation?

  • Float precision: AI versions often copy an approximate simplex noise (octave-boundary discontinuities). Here, the correct Ashima Arts (MIT) implementation.
  • devicePixelRatio: almost never handled → blurry on Retina. Our code computes the real resolution.
  • Shader error handling: compilation can fail silently. We log getShaderInfoLog and fall back.
  • Off-screen pause: requestAnimationFrame keeps running while invisible. Our IntersectionObserver stops the work in the background.
  • prefers-reduced-motion: an accessibility rule generators routinely forget.

Not guesswork: 5 issues we hit testing 12 ChatGPT-4.5 and v0.dev prompts — none of the results covered them all.

FAQ

Can I use it with React, Vue or Svelte?

Yes: vanilla JS, framework-agnostic. In React, wrap the logic in a useEffect with cleanup; in Svelte, onMount/onDestroy. No npm dependency.

Why a shader instead of a 2D canvas?

2D canvas computes each pixel on the CPU (ceiling ~50,000 animated pixels at 60fps). A WebGL shader runs in parallel on the GPU (millions of pixels per frame): raymarching, fluid simulation, post-processing become possible.

The full, ready-to-use code

This effect is free this month. Customize it live and copy-paste the code. Access 660+ more premium effects with the subscription.

Join the founders — €9.90 / month