Browse components
Aurora Background
Northern lights as folding canvas curtains: irregular ribbons and faint rays drifting across a night sky.
Add to your project
Pick the way you like to build.
Let your AI assistant add it
Copy this prompt and paste it into the AI tool building your site.
# Integrate this UI component
Add this component to my project.
1. Install any missing dependencies.
2. Make sure Tailwind CSS is set up and scans the new file.
3. Prefer passing props over editing the component source — changing the component itself may break it.
4. Adjust colors, size, and layout via props or wrapper classes to match my project. You know my codebase better than this snippet does.
## Component source
```tsx
'use client';
import { useEffect, useRef, type ReactNode } from 'react';
// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
//
// children Content layered above the aurora (headline, CTA, etc.)
// className Styles for the content wrapper
// containerClassName Styles for the outer shell (size, layout)
// theme "dark" (default) | "light"
// colors Aurora tones, sampled left → right across the sky.
// Keep these close in hue — one color's shades reads as
// a real aurora; a rainbow does not. Defaults per theme.
// intensity Overall brightness of the light
// speed "slow" (default) | "medium" | "fast"
//
type AuroraBackgroundProps = {
children?: ReactNode;
className?: string;
containerClassName?: string;
theme?: 'dark' | 'light';
colors?: string[];
intensity?: number;
speed?: 'slow' | 'medium' | 'fast';
};
const THEMES = {
dark: {
sky: '#04030f',
colors: ['#0f766e', '#15916b', '#34d399', '#6ee7b7', '#22c55e'],
blend: 'screen' as const,
},
light: {
sky: '#f2f6f4',
colors: ['#0d9488', '#059669', '#10b981', '#34d399', '#16a34a'],
blend: 'multiply' as const,
},
};
const SPEED_STEP = { slow: 0.006, medium: 0.01, fast: 0.018 } as const;
const randFrom = (n: number) => {
let seed = n;
return () => {
seed = (seed * 9301 + 49297) % 233280;
return seed / 233280;
};
};
type Band = {
kind: 'haze' | 'curtain';
y0: number;
y1: number;
colorT: number;
f1: number;
f2: number;
f3: number;
s1: number;
s2: number;
s3: number;
p1: number;
p2: number;
p3: number;
a1: number;
a2: number;
a3: number;
alpha: number;
folds: { u: number; w: number; p: number; s: number }[];
};
const BANDS: Band[] = (() => {
const rand = randFrom(31);
const bands: Band[] = [];
for (let i = 0; i < 2; i++) {
bands.push({
kind: 'haze',
y0: 0.2 + rand() * 0.08,
y1: 0.6 + rand() * 0.1,
colorT: 0.3 + i * 0.35,
f1: 2.2 + rand(),
f2: 4 + rand() * 1.6,
f3: 0.9 + rand() * 0.5,
s1: 0.3 + rand() * 0.2,
s2: 0.2 + rand() * 0.15,
s3: 0.12 + rand() * 0.1,
p1: rand() * Math.PI * 2,
p2: rand() * Math.PI * 2,
p3: rand() * Math.PI * 2,
a1: 22 + rand() * 12,
a2: 14 + rand() * 10,
a3: 18 + rand() * 10,
alpha: 0.32 + rand() * 0.1,
folds: [],
});
}
for (let i = 0; i < 3; i++) {
bands.push({
kind: 'curtain',
y0: 0.16 + rand() * 0.12,
y1: 0.54 + rand() * 0.16,
colorT: Math.min(1, Math.max(0, i / 2 + (rand() - 0.5) * 0.15)),
f1: 3.2 + rand() * 2.4,
f2: 6.5 + rand() * 3,
f3: 1.2 + rand(),
s1: 0.5 + rand() * 0.35,
s2: 0.32 + rand() * 0.28,
s3: 0.18 + rand() * 0.16,
p1: rand() * Math.PI * 2,
p2: rand() * Math.PI * 2,
p3: rand() * Math.PI * 2,
a1: 36 + rand() * 22,
a2: 22 + rand() * 16,
a3: 28 + rand() * 18,
alpha: 0.5 + rand() * 0.22,
folds: Array.from({ length: 5 + Math.floor(rand() * 3) }, () => ({
u: rand(),
w: 0.06 + rand() * 0.1,
p: rand() * Math.PI * 2,
s: 0.25 + rand() * 0.45,
})),
});
}
return bands;
})();
const hexA = (hex: string, a: number) => {
const n = hex.replace('#', '');
const r = parseInt(n.slice(0, 2), 16);
const g = parseInt(n.slice(2, 4), 16);
const b = parseInt(n.slice(4, 6), 16);
return `rgba(${r},${g},${b},${Math.max(0, Math.min(1, a))})`;
};
const pick = (palette: string[], t: number) =>
palette[Math.round(Math.min(1, Math.max(0, t)) * (palette.length - 1))];
const ribbonY = (
u: number,
t: number,
band: Band,
edge: 'top' | 'bot',
h: number
) => {
const wave =
Math.sin(u * band.f1 + t * band.s1 + band.p1) * band.a1 +
Math.sin(u * band.f2 - t * band.s2 + band.p2) * band.a2 +
Math.sin(u * band.f3 + t * band.s3 + band.p3) * band.a3;
const base = (edge === 'top' ? band.y0 : band.y1) * h;
return base + wave * (edge === 'top' ? 1 : 1.3);
};
export default function AuroraBackground({
children,
className = '',
containerClassName = '',
theme = 'dark',
colors,
intensity = 1,
speed = 'medium',
}: AuroraBackgroundProps) {
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const rafRef = useRef(0);
const timeRef = useRef(0);
const themeTokens = THEMES[theme] ?? THEMES.dark;
const palette = colors ?? themeTokens.colors;
useEffect(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
const ctx = canvas.getContext('2d', { alpha: false });
if (!ctx) return;
const step = SPEED_STEP[speed] ?? SPEED_STEP.medium;
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
// Half-res + CSS blur is the bloom. Never use ctx.filter (it stalls the tab).
const SCALE = 0.5;
let width = 0;
let height = 0;
const resize = () => {
const rect = container.getBoundingClientRect();
width = Math.max(1, Math.floor(rect.width * SCALE));
height = Math.max(1, Math.floor(rect.height * SCALE));
canvas.width = width;
canvas.height = height;
};
const traceRibbon = (band: Band, t: number) => {
ctx.beginPath();
const stepX = 10;
for (let x = -20; x <= width + 20; x += stepX) {
const y = ribbonY(x / width, t, band, 'top', height);
if (x === -20) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
for (let x = width + 20; x >= -20; x -= stepX) {
ctx.lineTo(x, ribbonY(x / width, t, band, 'bot', height));
}
ctx.closePath();
};
const drawRibbon = (band: Band, t: number) => {
const color = pick(palette, band.colorT);
const a = band.alpha * intensity;
const g = ctx.createLinearGradient(
0,
band.y0 * height - 20,
0,
band.y1 * height + 24
);
const body = band.kind === 'haze' ? a : a * 0.4;
g.addColorStop(0, hexA(color, 0));
g.addColorStop(0.22, hexA(color, body * 0.3));
g.addColorStop(0.48, hexA(color, body));
g.addColorStop(0.8, hexA(color, body * 0.35));
g.addColorStop(1, hexA(color, 0));
traceRibbon(band, t);
ctx.fillStyle = g;
ctx.fill();
if (band.kind !== 'curtain') return;
ctx.save();
traceRibbon(band, t);
ctx.clip();
for (const fold of band.folds) {
const x =
(fold.u + Math.sin(t * fold.s + fold.p) * 0.04) * width;
const pulse = 0.55 + 0.45 * Math.sin(t * fold.s * 1.4 + fold.p);
const fw = fold.w * width;
const fg = ctx.createLinearGradient(x - fw, 0, x + fw, 0);
fg.addColorStop(0, hexA(color, 0));
fg.addColorStop(0.5, hexA(color, a * 0.55 * pulse));
fg.addColorStop(1, hexA(color, 0));
ctx.fillStyle = fg;
ctx.fillRect(x - fw, 0, fw * 2, height);
}
ctx.restore();
};
const draw = () => {
timeRef.current += prefersReducedMotion ? 0 : step;
const t = timeRef.current;
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = themeTokens.sky;
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = themeTokens.blend;
for (const band of BANDS) drawRibbon(band, t);
ctx.globalCompositeOperation = 'source-over';
if (!prefersReducedMotion) {
rafRef.current = requestAnimationFrame(draw);
}
};
resize();
draw();
const observer = new ResizeObserver(resize);
observer.observe(container);
return () => {
cancelAnimationFrame(rafRef.current);
observer.disconnect();
};
}, [intensity, palette, speed, themeTokens.blend, themeTokens.sky]);
return (
<div
ref={containerRef}
className={[
'relative flex min-h-[480px] w-full flex-col items-center justify-center overflow-hidden',
containerClassName,
]
.filter(Boolean)
.join(' ')}
style={{ backgroundColor: themeTokens.sky }}
>
<canvas
ref={canvasRef}
className='absolute inset-0 z-0 h-full w-full scale-110 blur-[14px]'
aria-hidden
/>
<div
className={['relative z-10 px-6 text-center', className]
.filter(Boolean)
.join(' ')}
>
{children}
</div>
</div>
);
}
```
Works with your AI tools
* Compatible with most projects that use the default React/Next.js framework.
Built With
Props
| Prop | Type | Description |
|---|---|---|
children | ReactNode | Content layered above the aurora (headline, CTA, etc.) |
className | string | Styles for the content wrapper |
containerClassName | string | Styles for the outer shell (size, layout) |
theme | 'dark' | 'light' | Night sky or pale daylight ground |
colors | string[] | Aurora tones, sampled left → right across the sky |
intensity | number | Overall brightness of the light |
speed | 'slow' | 'medium' | 'fast' | How quickly the curtains fold and drift |
More Backgrounds Components

Animated canvas waves that fill the container and drift with the pointer — a softer, SaaS-ready take on classic wavy backgrounds.
Subtle canvas meteors drifting across a dark sky — a quiet animated background for heroes and landing sections.
Staggered neon trails that sweep across a dark field — colors are prop-driven for easy theming.
Soft layered waves in parallax drift — cool indigo tones by default, colors via props.
A static 3D background grid — a wireframe perspective grid that covers the full container, warped and bowed toward a vanishing point for depth. No animation, no layout shift.