library / backgrounds / aurora-borealis
live - 60fps 1920 x 1080
aurora-borealis.html - self-contained
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Aurora Borealis</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            overflow: hidden;
            background: #000208;
            height: 100vh;
        }

        canvas {
            display: block;
        }
    </style>
</head>

<body>
    <canvas id="canvas"></canvas>
    <script>
        const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');

        let W = canvas.width = window.innerWidth;
        let H = canvas.height = window.innerHeight;

        window.addEventListener('resize', () => {
            W = canvas.width = window.innerWidth;
            H = canvas.height = window.innerHeight;
            initStars();
            initNebula();
        });

        // ----- Mouse parallax -----
        let mouseNorm = 0; // -1 to 1
        window.addEventListener('mousemove', e => {
            mouseNorm = (e.clientX / W - 0.5) * 2;
        });

        // ----- Stars -----
        let stars = [];
        function initStars() {
            stars = [];
            for (let i = 0; i < 420; i++) {
                stars.push({
                    x: Math.random() * W,
                    y: Math.random() * H * 0.75,
                    size: Math.random() * 2.0 + 0.2,
                    twinkle: Math.random() * Math.PI * 2,
                    twinkleSpeed: Math.random() * 0.025 + 0.008,
                    brightness: Math.random() * 0.5 + 0.4
                });
            }
        }
        initStars();

        function drawStars() {
            for (const s of stars) {
                s.twinkle += s.twinkleSpeed;
                const alpha = s.brightness + Math.sin(s.twinkle) * 0.35;
                ctx.beginPath();
                ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
                ctx.fillStyle = `rgba(220, 240, 255, ${Math.max(0, Math.min(1, alpha))})`;
                ctx.fill();
            }
        }

        // ----- Nebula background glows -----
        let nebulae = [];
        function initNebula() {
            nebulae = [
                { x: W * 0.15, y: H * 0.2, r: W * 0.35, color: 'rgba(0,80,160,0.06)', speed: 0.0003 },
                { x: W * 0.75, y: H * 0.1, r: W * 0.30, color: 'rgba(80,0,160,0.05)', speed: 0.0004 },
                { x: W * 0.50, y: H * 0.35, r: W * 0.45, color: 'rgba(0,60,120,0.04)', speed: 0.0002 }
            ];
        }
        initNebula();
        let nebulaT = 0;

        function drawNebulae() {
            nebulaT += 0.004;
            for (const n of nebulae) {
                const ox = Math.sin(nebulaT * n.speed * 1000) * 40;
                const oy = Math.cos(nebulaT * n.speed * 1000 * 0.7) * 20;
                const grad = ctx.createRadialGradient(n.x + ox, n.y + oy, 0, n.x + ox, n.y + oy, n.r);
                grad.addColorStop(0, n.color);
                grad.addColorStop(1, 'transparent');
                ctx.fillStyle = grad;
                ctx.fillRect(0, 0, W, H);
            }
        }

        // ----- Aurora Bands -----
        const AURORA_BANDS = [
            { yBase: 0.20, amplitude: 55, freqA: 0.0028, freqB: 0.0065, freqC: 0.013, speedA: 0.007, speedB: 0.011, speedC: 0.004, color: [0, 255, 140], alpha: 0.30, thickness: 0.12 },
            { yBase: 0.28, amplitude: 45, freqA: 0.0035, freqB: 0.0080, freqC: 0.016, speedA: 0.010, speedB: 0.015, speedC: 0.006, color: [0, 190, 255], alpha: 0.25, thickness: 0.10 },
            { yBase: 0.38, amplitude: 35, freqA: 0.0045, freqB: 0.0055, freqC: 0.010, speedA: 0.006, speedB: 0.009, speedC: 0.003, color: [130, 0, 255], alpha: 0.20, thickness: 0.08 },
            { yBase: 0.48, amplitude: 28, freqA: 0.0060, freqB: 0.0100, freqC: 0.020, speedA: 0.009, speedB: 0.013, speedC: 0.007, color: [255, 0, 140], alpha: 0.15, thickness: 0.07 },
        ];

        let auroraTime = Array(AURORA_BANDS.length).fill(0);

        function drawAurora() {
            for (let b = 0; b < AURORA_BANDS.length; b++) {
                const band = AURORA_BANDS[b];
                auroraTime[b] += 1;
                const t = auroraTime[b];
                const T = t + mouseNorm * 18;

                const pts = [];
                const steps = 120;
                for (let i = 0; i <= steps; i++) {
                    const x = (i / steps) * W;
                    const wave = Math.sin(x * band.freqA + T * band.speedA) * band.amplitude
                        + Math.sin(x * band.freqB + T * band.speedB) * (band.amplitude * 0.55)
                        + Math.sin(x * band.freqC + T * band.speedC) * (band.amplitude * 0.25);
                    pts.push({ x, y: H * band.yBase + wave });
                }

                const bandH = H * band.thickness;
                const [r, g, bl] = band.color;

                // Render upward sweep (sky glow)
                ctx.beginPath();
                ctx.moveTo(0, H * band.yBase - bandH * 0.5);
                for (let i = 0; i <= steps; i++) {
                    if (i === 0) ctx.lineTo(pts[i].x, pts[i].y - bandH);
                    else {
                        const prev = pts[i - 1];
                        const cx = (prev.x + pts[i].x) / 2;
                        const cy = ((prev.y - bandH) + (pts[i].y - bandH)) / 2;
                        ctx.quadraticCurveTo(prev.x, prev.y - bandH, cx, cy);
                    }
                }
                for (let i = steps; i >= 0; i--) {
                    const prev = i < steps ? pts[i + 1] : pts[steps];
                    const cx = i < steps ? (prev.x + pts[i].x) / 2 : pts[steps].x;
                    const cy = i < steps ? (prev.y + pts[i].y) / 2 : pts[steps].y;
                    if (i === steps) ctx.lineTo(pts[i].x, pts[i].y);
                    else ctx.quadraticCurveTo(prev.x, prev.y, cx, cy);
                }
                ctx.closePath();

                const grad = ctx.createLinearGradient(0, pts[0].y - bandH, 0, pts[0].y + bandH * 0.5);
                grad.addColorStop(0, `rgba(${r},${g},${bl},0)`);
                grad.addColorStop(0.3, `rgba(${r},${g},${bl},${band.alpha})`);
                grad.addColorStop(0.6, `rgba(${r},${g},${bl},${band.alpha * 0.6})`);
                grad.addColorStop(1, `rgba(${r},${g},${bl},0)`);
                ctx.fillStyle = grad;
                ctx.fill();
            }
        }

        // ----- Shooting Stars -----
        let shooters = [];
        let nextShooter = 4000 + Math.random() * 5000;
        let shooterTimer = 0;

        function spawnShooter() {
            shooters.push({
                x: Math.random() * W * 0.6,
                y: Math.random() * H * 0.3,
                vx: 8 + Math.random() * 6,
                vy: 3 + Math.random() * 4,
                life: 1.0,
                decay: 0.025 + Math.random() * 0.02
            });
        }

        function drawShooters(dt) {
            shooterTimer += dt;
            if (shooterTimer > nextShooter) {
                spawnShooter();
                shooterTimer = 0;
                nextShooter = 4000 + Math.random() * 6000;
            }

            shooters = shooters.filter(s => s.life > 0);
            for (const s of shooters) {
                const tailLen = 80 * s.life;
                const grad = ctx.createLinearGradient(s.x - s.vx * 4, s.y - s.vy * 4, s.x, s.y);
                grad.addColorStop(0, 'transparent');
                grad.addColorStop(1, `rgba(255, 255, 255, ${s.life * 0.9})`);
                ctx.beginPath();
                ctx.moveTo(s.x - s.vx * (tailLen / s.vx), s.y - s.vy * (tailLen / s.vy));
                ctx.lineTo(s.x, s.y);
                ctx.strokeStyle = grad;
                ctx.lineWidth = 1.5 * s.life;
                ctx.stroke();

                s.x += s.vx;
                s.y += s.vy;
                s.life -= s.decay;
            }
        }

        let last = 0;
        function animate(ts) {
            const dt = ts - last;
            last = ts;

            ctx.fillStyle = '#000208';
            ctx.fillRect(0, 0, W, H);

            drawNebulae();
            drawStars();
            drawAurora();
            drawShooters(dt);

            requestAnimationFrame(animate);
        }

        requestAnimationFrame(animate);
    </script>
</body>

</html>

About this animation

Premium northern lights simulation with sky-positioned layered gradients, twinkling stars, and shooting stars.

Under the hood

Hardware Accelerated CSS Blobs

This avoids heavy canvas math entirely. Instead, three distinct HTML elements are styled with a massive filter: blur(60px) and overlaid using mix-blend-mode: screen. The CSS @keyframes translate and rotate these shapes seamlessly on the GPU, creating an ethereal, flowing aurora.

Source Code
Source copied
Partner hosting options for the copied effect:
Vercel Netlify Hostinger