Aurora
Flowing aurora gradient background.
pnpm dlx shadcn@latest add @leo/auroraProps
| Name | Type | Default | Description |
|---|---|---|---|
| amplitude | number | undefined | - | |
| blend | number | undefined | - | |
| className | string | undefined | - | |
| colorStops | string[] | undefined | - | |
| lightMode | boolean | undefined | - | |
| speed | number | undefined | - | |
| time | number | undefined | - |
Dependencies
- ogl@^1.0.11
Source
"use client";
/*
* Ported from react-bits (MIT, © David Haz): src/ts-tailwind/Backgrounds/Aurora/Aurora.tsx @ 9d0270d
*
* MIT License
*
* Copyright (c) 2026 David Haz
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { Color, Mesh, Program, Renderer, Triangle } from "ogl";
import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
const VERT = `#version 300 es
in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const FRAG = `#version 300 es
precision highp float;
uniform float uTime;
uniform float uAmplitude;
uniform vec3 uColorStops[3];
uniform vec2 uResolution;
uniform float uBlend;
uniform float uLightMode;
out vec4 fragColor;
vec3 permute(vec3 x) {
return mod(((x * 34.0) + 1.0) * x, 289.0);
}
float snoise(vec2 v){
const vec4 C = vec4(
0.211324865405187, 0.366025403784439,
-0.577350269189626, 0.024390243902439
);
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod(i, 289.0);
vec3 p = permute(
permute(i.y + vec3(0.0, i1.y, 1.0))
+ i.x + vec3(0.0, i1.x, 1.0)
);
vec3 m = max(
0.5 - vec3(
dot(x0, x0),
dot(x12.xy, x12.xy),
dot(x12.zw, x12.zw)
),
0.0
);
m = m * m;
m = m * m;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
struct ColorStop {
vec3 color;
float position;
};
#define COLOR_RAMP(colors, factor, finalColor) { \
int index = 0; \
for (int i = 0; i < 2; i++) { \
ColorStop currentColor = colors[i]; \
bool isInBetween = currentColor.position <= factor; \
index = int(mix(float(index), float(i), float(isInBetween))); \
} \
ColorStop currentColor = colors[index]; \
ColorStop nextColor = colors[index + 1]; \
float range = nextColor.position - currentColor.position; \
float lerpFactor = (factor - currentColor.position) / range; \
finalColor = mix(currentColor.color, nextColor.color, lerpFactor); \
}
void main() {
vec2 uv = gl_FragCoord.xy / uResolution;
ColorStop colors[3];
colors[0] = ColorStop(uColorStops[0], 0.0);
colors[1] = ColorStop(uColorStops[1], 0.5);
colors[2] = ColorStop(uColorStops[2], 1.0);
vec3 rampColor;
COLOR_RAMP(colors, uv.x, rampColor);
float height = snoise(vec2(uv.x * 2.0 + uTime * 0.1, uTime * 0.25)) * 0.5 * uAmplitude;
height = exp(height);
height = (uv.y * 2.0 - height + 0.2);
float intensity = 0.6 * height;
float midPoint = 0.20;
float auroraAlpha = smoothstep(midPoint - uBlend * 0.5, midPoint + uBlend * 0.5, intensity);
vec3 auroraColor = intensity * rampColor;
if (uLightMode > 0.5) {
float energy = clamp(max(intensity, 0.0), 0.0, 1.0);
float coverage = clamp(auroraAlpha * (0.55 + 0.45 * energy), 0.0, 0.86);
vec3 chroma = pow(clamp(rampColor, 0.0, 1.0), vec3(1.2));
float chromaPeak = max(chroma.r, max(chroma.g, chroma.b));
chroma /= max(chromaPeak, 0.0001);
fragColor = vec4(mix(vec3(1.0), chroma, min(coverage * 1.08, 0.94)), 1.0);
} else {
fragColor = vec4(auroraColor * auroraAlpha, auroraAlpha);
}
}
`;
export interface AuroraProps {
className?: string;
colorStops?: string[];
amplitude?: number;
blend?: number;
time?: number;
speed?: number;
lightMode?: boolean;
}
export function Aurora(props: AuroraProps) {
const {
colorStops = ["#5227FF", "#7cff67", "#5227FF"],
amplitude = 1.0,
blend = 0.5,
lightMode = false,
} = props;
const propsRef = useRef<AuroraProps>(props);
propsRef.current = props;
const ctnDom = useRef<HTMLDivElement>(null);
// biome-ignore lint/correctness/useExhaustiveDependencies: the effect reads live values via propsRef; only amplitude re-creates the renderer
useEffect(() => {
const ctn = ctnDom.current;
if (!ctn) return;
const renderer = new Renderer({
alpha: true,
premultipliedAlpha: true,
antialias: true,
});
const gl = renderer.gl;
gl.clearColor(0, 0, 0, 0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.canvas.style.backgroundColor = "transparent";
let program: Program | undefined;
function resize() {
if (!ctn) return;
const width = ctn.offsetWidth;
const height = ctn.offsetHeight;
renderer.setSize(width, height);
if (program) {
program.uniforms.uResolution.value = [width, height];
}
}
window.addEventListener("resize", resize);
const geometry = new Triangle(gl);
if (geometry.attributes.uv) {
delete geometry.attributes.uv;
}
const colorStopsArray = colorStops.map((hex) => {
const c = new Color(hex);
return [c.r, c.g, c.b];
});
program = new Program(gl, {
vertex: VERT,
fragment: FRAG,
uniforms: {
uTime: { value: 0 },
uAmplitude: { value: amplitude },
uColorStops: { value: colorStopsArray },
uResolution: { value: [ctn.offsetWidth, ctn.offsetHeight] },
uBlend: { value: blend },
uLightMode: { value: lightMode ? 1 : 0 },
},
});
const mesh = new Mesh(gl, { geometry, program });
ctn.appendChild(gl.canvas);
let animateId = 0;
const update = (t: number) => {
animateId = requestAnimationFrame(update);
const { time = t * 0.01, speed = 1.0 } = propsRef.current;
if (program) {
program.uniforms.uTime.value = time * speed * 0.1;
program.uniforms.uAmplitude.value = propsRef.current.amplitude ?? 1.0;
program.uniforms.uBlend.value = propsRef.current.blend ?? blend;
program.uniforms.uLightMode.value = (propsRef.current.lightMode ?? lightMode) ? 1 : 0;
const stops = propsRef.current.colorStops ?? colorStops;
program.uniforms.uColorStops.value = stops.map((hex: string) => {
const c = new Color(hex);
return [c.r, c.g, c.b];
});
renderer.render({ scene: mesh });
}
};
animateId = requestAnimationFrame(update);
resize();
return () => {
cancelAnimationFrame(animateId);
window.removeEventListener("resize", resize);
if (ctn && gl.canvas.parentNode === ctn) {
ctn.removeChild(gl.canvas);
}
gl.getExtension("WEBGL_lose_context")?.loseContext();
};
}, [amplitude]);
return <div ref={ctnDom} className={cn("w-full h-full", props.className)} />;
}
export default Aurora;