Web Animations Hard
Shader-lit Hero
A raw fragment shader paints a moving gradient hero with no external renderer or dependency.
Open in Lab
MCP
html css javascript react
Targets: TS JS HTML React
Code
:root {
--bg: #07080c;
--fg: #f2f4f8;
--muted: #9aa3b5;
--line: rgba(255, 255, 255, 0.12);
color-scheme: dark;
}
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--fg);
font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
}
button, input { font: inherit; }
.demo {
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(1rem, 4vw, 3rem);
}
.hero {
position: relative;
width: min(920px, 100%);
min-height: 460px;
border-radius: 24px;
overflow: hidden;
border: 1px solid var(--line);
isolation: isolate;
box-shadow: 0 30px 90px rgba(0, 0, 0, 0.6);
}
.hero__canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
z-index: 0;
}
.hero::after {
content: "";
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
background: linear-gradient(105deg, rgba(4,5,9,.9) 0%, rgba(4,5,9,.55) 48%, rgba(4,5,9,.08) 100%);
}
.hero__content {
position: relative;
z-index: 2;
padding: clamp(28px, 5vw, 56px);
max-width: 640px;
}
.hero__eyebrow {
margin: 0 0 14px;
font-size: 12px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--muted);
}
.hero__title {
margin: 0 0 16px;
font-size: clamp(34px, 6vw, 58px);
line-height: 1.03;
letter-spacing: -.03em;
font-weight: 700;
}
.hero__lede {
margin: 0 0 28px;
color: #cbd2e0;
line-height: 1.65;
max-width: 46ch;
}
.hero__actions {
display: flex;
flex-wrap: wrap;
gap: 14px;
align-items: center;
}
.btn {
font-weight: 600;
font-size: 14px;
padding: 11px 22px;
border-radius: 999px;
border: 1px solid transparent;
cursor: pointer;
transition: transform 140ms ease;
}
.btn--primary { background: var(--fg); color: #0a0b10; }
.btn--primary:hover { transform: translateY(-1px); }
.btn:focus-visible, input:focus-visible { outline: 2px solid #7dd3fc; outline-offset: 3px; }
.field {
display: flex;
align-items: center;
gap: 9px;
font-size: 12px;
color: var(--muted);
background: rgba(255, 255, 255, .05);
border: 1px solid var(--line);
padding: 7px 14px;
border-radius: 999px;
backdrop-filter: blur(8px);
}
.field__label { letter-spacing: .08em; text-transform: uppercase; }
.field input[type="range"] { width: 92px; accent-color: #7dd3fc; }
.field output { font-variant-numeric: tabular-nums; color: var(--fg); min-width: 44px; }
.hero__status { margin: 22px 0 0; font-size: 12px; color: var(--muted); }
.hero__status[data-state="error"] { color: #fca5a5; }
@media (prefers-reduced-motion: reduce) {
.btn { transition: none; }
}// Shader-lit Hero — raw WebGL fragment shader, zero dependencies.
(function () {
var canvas = document.getElementById("shader");
var status = document.getElementById("status");
var toggle = document.getElementById("toggle");
var speedIn = document.getElementById("speed");
var speedOut = document.getElementById("speedOut");
var hueIn = document.getElementById("hue");
var hueOut = document.getElementById("hueOut");
var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
if (!gl) {
status.textContent = "WebGL unavailable — showing a static gradient fallback.";
status.dataset.state = "error";
canvas.style.background =
"radial-gradient(120% 120% at 75% 25%, #2b6cb0 0%, #4c1d95 45%, #07080c 100%)";
toggle.disabled = true;
speedIn.disabled = true;
hueIn.disabled = true;
return;
}
var VERT = [
"attribute vec2 aPos;",
"void main(){ gl_Position = vec4(aPos, 0.0, 1.0); }"
].join("\n");
// Domain-warped fbm noise, colour rotated by a hue uniform.
var FRAG = [
"precision highp float;",
"uniform vec2 uRes;",
"uniform float uTime;",
"uniform float uHue;",
"",
"float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }",
"float noise(vec2 p){",
" vec2 i = floor(p), f = fract(p);",
" vec2 u = f * f * (3.0 - 2.0 * f);",
" return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x),",
" mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y);",
"}",
"float fbm(vec2 p){",
" float v = 0.0, a = 0.5;",
" for (int i = 0; i < 5; i++){ v += a * noise(p); p *= 2.02; a *= 0.5; }",
" return v;",
"}",
"vec3 hsl2rgb(vec3 c){",
" vec3 k = mod(c.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0);",
" vec3 rgb = clamp(min(k, 4.0 - k), 0.0, 1.0);",
" return c.z + c.y * (rgb - 0.5) * (1.0 - abs(2.0 * c.z - 1.0));",
"}",
"void main(){",
" vec2 uv = (gl_FragCoord.xy - 0.5 * uRes) / min(uRes.x, uRes.y);",
" float t = uTime;",
" vec2 q = vec2(fbm(uv * 2.0 + t * 0.08), fbm(uv * 2.0 + vec2(5.2, 1.3) - t * 0.06));",
" vec2 r = vec2(fbm(uv * 2.4 + 3.5 * q + t * 0.05), fbm(uv * 2.4 + 3.5 * q + vec2(8.3, 2.8)));",
" float f = fbm(uv * 2.0 + 3.2 * r);",
" float hue = fract(uHue + f * 0.22 + r.x * 0.12);",
" float sat = 0.62 + 0.25 * r.y;",
" float lum = 0.10 + 0.55 * pow(f, 1.4);",
" vec3 col = hsl2rgb(vec3(hue, sat, lum));",
" col += pow(max(0.0, 1.0 - length(uv - vec2(0.55, 0.30))), 4.0) * 0.35;",
" col *= 1.0 - 0.35 * length(uv) * 0.6;", // gentle vignette
" col += (hash(gl_FragCoord.xy + t) - 0.5) * 0.02;", // dither
" gl_FragColor = vec4(col, 1.0);",
"}"
].join("\n");
function compile(type, src) {
var s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(s) || "shader compile failed");
}
return s;
}
var program;
try {
program = gl.createProgram();
gl.attachShader(program, compile(gl.VERTEX_SHADER, VERT));
gl.attachShader(program, compile(gl.FRAGMENT_SHADER, FRAG));
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(program) || "link failed");
}
} catch (err) {
status.textContent = "Shader error: " + err.message;
status.dataset.state = "error";
return;
}
gl.useProgram(program);
var buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
var aPos = gl.getAttribLocation(program, "aPos");
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
var uRes = gl.getUniformLocation(program, "uRes");
var uTime = gl.getUniformLocation(program, "uTime");
var uHue = gl.getUniformLocation(program, "uHue");
function resize() {
var dpr = Math.min(window.devicePixelRatio || 1, 2);
var w = Math.max(1, Math.round(canvas.clientWidth * dpr));
var h = Math.max(1, Math.round(canvas.clientHeight * dpr));
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w;
canvas.height = h;
gl.viewport(0, 0, w, h);
}
gl.uniform2f(uRes, canvas.width, canvas.height);
}
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)");
var running = !reduce.matches;
var speed = parseFloat(speedIn.value);
var hue = parseFloat(hueIn.value) / 360;
var shaderTime = 0;
var last = 0;
var raf = 0;
function render(now) {
var dt = last ? (now - last) / 1000 : 0;
last = now;
if (running) shaderTime += dt * speed;
resize();
gl.uniform1f(uTime, shaderTime);
gl.uniform1f(uHue, hue);
gl.drawArrays(gl.TRIANGLES, 0, 3);
raf = requestAnimationFrame(render);
}
function setRunning(next) {
running = next;
toggle.textContent = running ? "Pause" : "Play";
toggle.setAttribute("aria-pressed", String(running));
status.textContent = running ? "WebGL active" : "Animation paused";
}
toggle.addEventListener("click", function () { setRunning(!running); });
speedIn.addEventListener("input", function () {
speed = parseFloat(speedIn.value);
speedOut.textContent = speed.toFixed(1) + "×";
});
hueIn.addEventListener("input", function () {
hue = parseFloat(hueIn.value) / 360;
hueOut.textContent = hueIn.value + "°";
});
reduce.addEventListener("change", function (e) { if (e.matches) setRunning(false); });
document.addEventListener("visibilitychange", function () {
if (document.hidden) {
cancelAnimationFrame(raf);
raf = 0;
} else if (!raf) {
last = 0;
raf = requestAnimationFrame(render);
}
});
setRunning(running);
resize();
raf = requestAnimationFrame(render);
})();<link rel="stylesheet" href="style.css">
<main class="demo" data-demo="Shader-lit Hero">
<section class="hero">
<canvas id="shader" class="hero__canvas" aria-hidden="true"></canvas>
<div class="hero__content">
<p class="hero__eyebrow">Raw fragment shader · no dependencies</p>
<h1 class="hero__title">Light from a few lines.</h1>
<p class="hero__lede">
A single GLSL fragment shader paints this moving gradient — no renderer,
no library, just a canvas, a WebGL context and a render loop.
</p>
<div class="hero__actions">
<button type="button" class="btn btn--primary" id="toggle" aria-pressed="true">Pause</button>
<label class="field">
<span class="field__label">Speed</span>
<input id="speed" type="range" min="0.1" max="3" step="0.1" value="1">
<output id="speedOut" for="speed">1.0×</output>
</label>
<label class="field">
<span class="field__label">Hue</span>
<input id="hue" type="range" min="0" max="360" step="1" value="215">
<output id="hueOut" for="hue">215°</output>
</label>
</div>
<p class="hero__status" id="status" role="status">WebGL active</p>
</div>
</section>
</main>
<script src="script.js"></script>import { useEffect, useRef, useState } from "react";
export function ThreeShaderHero() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [active, setActive] = useState(false);
useEffect(() => {
const canvas = canvasRef.current;
const gl = canvas?.getContext("webgl");
if (gl) {
gl.clearColor(0.06, 0.1, 0.22, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
}
}, []);
return (
<section className="demo">
<h2>Shader-lit Hero</h2>
<canvas
ref={canvasRef}
style={{ width: "100%", height: 260, background: "#080d18" }}
onPointerMove={() => setActive(true)}
/>
<button onClick={() => setActive((v) => !v)}>{active ? "Active" : "Activate"}</button>
</section>
);
}Shader-lit Hero
A raw fragment shader paints a moving gradient hero with no external renderer or dependency.
Support notes
The WebGL examples deliberately stay raw: a canvas, context, and render loop. Use this as a small foundation for a real shader or add a renderer only when scene complexity warrants it.
Included demo
- Vanilla HTML, CSS, and JavaScript with zero external dependencies.
- React equivalent using the same interaction model.
- A Lab route for trying the behavior in isolation.
Integration checklist
Keep state updates separate from presentation, preserve semantic labels, and add persistence or server callbacks at the boundary where your product needs them.