Northern-lights WebGL shader with starfield. Hero-ready.
An aurora borealis effect rendered in a fragment shader. Multiple flowing color bands (green-teal + violet-purple) drift across a starry night sky. Hero-ready atmospheric background that doesn't repeat or look like a cliché AI gradient.
aurora-hero · intensity awwwards
Base-catalogue pattern — no generated axes.
Generation prompts are part of a CreativePower subscription, and are never public.
See plans.au-stage {
position: relative;
min-height: 100vh;
overflow: hidden;
/* Dark ground derived from the spec (stageMode "dark"): the bands are additive light,
so they need a night field to read as aurora rather than as smudge. */
--au-ink: color-mix(in oklab, var(--color-on-surface, #050510) 92%, var(--color-surface, #050510));
/* The shader reads these four through the runtime probe, so the animated sky and the
reduced-motion gradient below are the same palette rather than two hand-matched sets.
Each carries its own original value as the fallback on BOTH sides of the mix. */
--au-sky-lo: var(--au-ink);
--au-sky-hi: color-mix(in oklab, var(--color-on-surface, #0d051f) 88%, var(--color-secondary, var(--color-primary, #0d051f)));
--au-band-a: color-mix(in oklab, var(--color-primary, #66f2b2) 80%, var(--color-surface, #66f2b2));
--au-band-b: color-mix(in oklab, var(--color-secondary, var(--color-primary, #9966ff)) 80%, var(--color-surface, #9966ff));
/* Headline halo — its own literal, one step off the band because it was tuned by eye. */
--au-glow: color-mix(in oklab, var(--color-secondary, var(--color-primary, #9664ff)) 80%, var(--color-surface, #9664ff));
/* Still frames of the two band colours over the ink, for the reduced-motion gradient. */
--au-veil-a: color-mix(in oklab, var(--color-on-surface, #1a0d3a) 88%, var(--color-secondary, var(--color-primary, #1a0d3a)));
--au-veil-b: color-mix(in oklab, var(--color-on-surface, #0d2e3a) 88%, var(--color-primary, #0d2e3a));
background: var(--au-ink);
display: flex;
align-items: center;
justify-content: center;
}
@supports (color: oklch(from red min(l, 1 - l) c h)) {
.au-stage {
--au-ds-ground: oklch(from color-mix(in oklab, var(--color-on-surface) 92%, var(--color-surface)) min(l, 1 - l) c h);
--au-ds-shade: oklch(from var(--color-on-surface) min(l, 1 - l) c h);
--au-ds-ink: oklch(from var(--color-surface) max(l, 1 - l) c h);
--au-ink: var(--au-ds-ground, color-mix(in oklab, var(--color-on-surface, #050510) 92%, var(--color-surface, #050510)));
--au-sky-hi: color-mix(in oklab, var(--au-ds-shade, #0d051f) 88%, var(--color-secondary, var(--color-primary, #0d051f)));
--au-band-a: color-mix(in oklab, var(--color-primary, #66f2b2) 80%, var(--au-ds-ink, #66f2b2));
--au-band-b: color-mix(in oklab, var(--color-secondary, var(--color-primary, #9966ff)) 80%, var(--au-ds-ink, #9966ff));
--au-glow: color-mix(in oklab, var(--color-secondary, var(--color-primary, #9664ff)) 80%, var(--au-ds-ink, #9664ff));
--au-veil-a: color-mix(in oklab, var(--au-ds-shade, #1a0d3a) 88%, var(--color-secondary, var(--color-primary, #1a0d3a)));
--au-veil-b: color-mix(in oklab, var(--au-ds-shade, #0d2e3a) 88%, var(--color-primary, #0d2e3a));
}
}
.au-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.au-content {
position: relative;
z-index: 1;
text-align: center;
padding: 0 5%;
/* stageMode "dark": content sits on the dark stage, so it takes the SURFACE colour. */
color: var(--color-surface, #fff);
}
@supports (color: oklch(from red min(l, 1 - l) c h)) {
.au-content {
--au-ds-ink: oklch(from var(--color-surface) max(l, 1 - l) c h);
color: var(--au-ds-ink, #fff);
}
}
.au-title {
font-family: 'Geist', sans-serif;
font-size: clamp(2.5rem, 6vw, 5rem);
font-weight: 700;
letter-spacing: -0.04em;
margin: 0 0 1rem;
text-shadow: 0 0 40px color-mix(in oklab, var(--au-glow) 40%, transparent);
}
.au-sub {
font-family: 'Geist Mono', monospace;
font-size: 0.8rem;
letter-spacing: 0.1em;
text-transform: uppercase;
opacity: 0.7;
}
@media (prefers-reduced-motion: reduce) {
.au-canvas { display: none; }
/* Static fallback: the same ink and the same two band hues, held still. */
.au-stage { background: linear-gradient(180deg,
var(--au-ink) 0%, var(--au-veil-a) 40%, var(--au-veil-b) 70%, var(--au-ink) 100%); }
}(function(){
const canvas = document.querySelector('.au-canvas');
if (!canvas) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) return;
const vs = `attribute vec2 a_pos; void main(){ gl_Position = vec4(a_pos, 0.0, 1.0); }`;
const fs = `precision mediump float;
uniform float u_time;
uniform vec2 u_res;
uniform vec3 u_sky0; // --au-sky-lo
uniform vec3 u_sky1; // --au-sky-hi
uniform vec3 u_bandA; // --au-band-a
uniform vec3 u_bandB; // --au-band-b
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);
float a = hash(i), b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0)), d = hash(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}
float fbm(vec2 p){
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) { v += a * noise(p); p *= 2.0; a *= 0.5; }
return v;
}
void main(){
vec2 uv = gl_FragCoord.xy / u_res;
float t = u_time * 0.30; // was 0.08 — far too slow
vec3 sky = mix(u_sky0, u_sky1, uv.y);
float band1 = fbm(vec2(uv.x * 3.0 + t, uv.y * 1.5 - t * 0.5));
float band2 = fbm(vec2(uv.x * 5.0 - t * 0.7, uv.y * 2.0 + t * 0.3));
float vMask = smoothstep(0.1, 0.5, uv.y) * (1.0 - smoothstep(0.5, 0.95, uv.y));
float aurora1 = pow(band1, 2.0) * vMask * 1.9;
float aurora2 = pow(band2, 3.0) * vMask * 1.5;
vec3 colA = u_bandA * aurora1;
vec3 colB = u_bandB * aurora2;
vec3 col = sky + colA + colB;
// Twinkling star field
float tw = 0.6 + 0.4 * sin(u_time * 2.0 + hash(floor(uv * u_res * 0.5)) * 30.0);
float stars = step(0.995, hash(floor(uv * u_res * 0.5)));
col += stars * 0.7 * tw * (1.0 - uv.y * 0.5);
gl_FragColor = vec4(col, 1.0);
}`;
function compile(src, type){
const sh = gl.createShader(type);
gl.shaderSource(sh, src);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(sh)); return null; }
return sh;
}
const prog = gl.createProgram();
const v = compile(vs, gl.VERTEX_SHADER);
const f = compile(fs, gl.FRAGMENT_SHADER);
if (!v || !f) return;
gl.attachShader(prog, v);
gl.attachShader(prog, f);
gl.linkProgram(prog);
gl.useProgram(prog);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
const a_pos = gl.getAttribLocation(prog, 'a_pos');
gl.enableVertexAttribArray(a_pos);
gl.vertexAttribPointer(a_pos, 2, gl.FLOAT, false, 0, 0);
const u_time = gl.getUniformLocation(prog, 'u_time');
const u_res = gl.getUniformLocation(prog, 'u_res');
const stage = canvas.closest('.au-stage') || canvas.parentElement;
// ── Token resolution ───────────────────────────────────────────────────
// Canvas and WebGL take numbers, not var(). Resolve the tokens once, through the
// browser: a hidden probe forces the cascade to compute the declaration, so
// color-mix() and any token indirection arrive here already serialized as
// rgb()/rgba(). With no tokens declared every fallback is the pattern's original
// literal, so the painted result is unchanged.
const TOKEN_ROOT = (stage) || document.body;
function computed(prop, decl, fallback){
const probe = document.createElement('span');
probe.style.cssText = 'position:absolute;left:-9999px;width:0;height:0;';
probe.style.setProperty(prop, decl);
(TOKEN_ROOT || document.body).appendChild(probe);
const v = getComputedStyle(probe)[prop];
probe.remove();
return v || fallback;
}
function rgbOf(decl, fallback){
const v = computed('color', decl, null);
if (!v) return fallback;
// DO NOT parse the numbers out of this string. A color-mix(in oklab, ...) serializes
// as 'oklab(L a b)', so a naive match() would read lightness as a red channel and
// every derived colour would collapse to near-black. Paint it and read the real
// 8-bit sRGB back instead — that works for rgb(), oklab(), lab(), color(), any of it.
try {
const c = document.createElement('canvas');
c.width = 1; c.height = 1;
const x = c.getContext('2d', { willReadFrequently: true });
x.fillStyle = '#000';
x.fillStyle = v; // an unparseable value leaves fillStyle at #000
x.fillRect(0, 0, 1, 1);
const d = x.getImageData(0, 0, 1, 1).data;
return [ d[0], d[1], d[2] ];
} catch (e) {}
// No 2D context (or a hostile embed). Fall back to the legacy rgb() shape, which is
// still what a plain var() with a hex fallback resolves to.
const m = String(v).match(/^rgba?\(([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/);
return m ? [ +m[1], +m[2], +m[3] ] : fallback;
}
function rgb01(decl, fallback){
const c = rgbOf(decl, null);
return c ? [ c[0]/255, c[1]/255, c[2]/255 ] : fallback;
}
function hexOf(decl, fallback){
const c = rgbOf(decl, null);
return c ? ((c[0]|0) << 16) + ((c[1]|0) << 8) + (c[2]|0) : fallback;
}
// Four constant colours, uploaded once. They mirror the stage's own custom properties,
// so the reduced-motion gradient in CSS and the live sky can never drift apart.
gl.uniform3fv(gl.getUniformLocation(prog, 'u_sky0'), rgb01('var(--au-sky-lo, #050510)', [0.02, 0.02, 0.06]));
gl.uniform3fv(gl.getUniformLocation(prog, 'u_sky1'), rgb01('var(--au-sky-hi, #0d051f)', [0.05, 0.02, 0.12]));
gl.uniform3fv(gl.getUniformLocation(prog, 'u_bandA'), rgb01('var(--au-band-a, #66f2b2)', [0.4, 0.95, 0.7]));
gl.uniform3fv(gl.getUniformLocation(prog, 'u_bandB'), rgb01('var(--au-band-b, #9966ff)', [0.6, 0.4, 1.0]));
function resize(){
const dpr = Math.min(window.devicePixelRatio, 2);
canvas.width = canvas.clientWidth * dpr;
canvas.height = canvas.clientHeight * dpr;
gl.viewport(0, 0, canvas.width, canvas.height);
}
resize();
window.addEventListener('resize', resize);
let start = performance.now();
function tick(){
const t = (performance.now() - start) / 1000;
gl.uniform1f(u_time, t);
gl.uniform2f(u_res, canvas.width, canvas.height);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(tick);
}
tick();
})();<section class="au-stage">
<canvas class="au-canvas"></canvas>
<div class="au-content">
<h1 class="au-title">A new frontier of design</h1>
<p class="au-sub">— Aurora hero · WebGL shader</p>
</div>
</section>