← all components
#08

Aurora flow background

A living aurora made of layered, sine-driven ribbons painted on canvas with additive blending, so where the ribbons overlap they brighten into real northern lights. It breathes on its own, and your cursor warps the field like a gust of wind. A premium hero or section background in React or plain HTML.

ReactCanvasJSHTML
// LIVE DEMO

Northern lights

Move your cursor to bend the aurora.

share this component →FacebookX / TwitterLinkedIn
// THE CODE
React
"use client";
import { useEffect, useRef } from "react";

export default function AuroraFlow() {
  const ref = useRef(null);
  useEffect(() => {
    const cv = ref.current, ctx = cv.getContext("2d");
    if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    let w, h, raf, t = 0;
    const DPR = Math.min(devicePixelRatio || 1, 2);
    const m = { x: .5, y: .5, tx: .5, ty: .5 };
    const R = [
      { hue: 190, amp: .14, speed: .5, y: .42, w: .20 },
      { hue: 250, amp: .11, speed: .7, y: .52, w: .16 },
      { hue: 300, amp: .09, speed: .9, y: .60, w: .13 },
    ];
    function resize() {
      const r = cv.getBoundingClientRect(); w = r.width; h = r.height;
      cv.width = w * DPR; cv.height = h * DPR; ctx.setTransform(DPR,0,0,DPR,0,0);
    }
    function frame() {
      t += .006;
      m.x += (m.tx - m.x) * .06; m.y += (m.ty - m.y) * .06;
      ctx.globalCompositeOperation = "source-over";
      ctx.fillStyle = "#05070f"; ctx.fillRect(0,0,w,h);
      ctx.globalCompositeOperation = "lighter";
      const wx = (m.x - .5) * .6, wy = (m.y - .5) * .4;
      for (const rb of R) {
        ctx.beginPath();
        const n = 48;
        for (let i = 0; i <= n; i++) {
          const px = i / n;
          const wave = Math.sin(px*6 + t*rb.speed*3)*rb.amp + Math.sin(px*11 - t*rb.speed*2)*rb.amp*.5;
          const bend = wx * Math.sin(px * Math.PI);
          const y = (rb.y + wave + wy*.3 + bend*.15) * h, x = px * w;
          i ? ctx.lineTo(x,y) : ctx.moveTo(x,y);
        }
        const my = (rb.y + wy*.3) * h;
        const g = ctx.createLinearGradient(0, my - rb.w*h, 0, my + rb.w*h);
        g.addColorStop(0, `hsla(${rb.hue},90%,60%,0)`);
        g.addColorStop(.5, `hsla(${rb.hue},95%,65%,.28)`);
        g.addColorStop(1, `hsla(${rb.hue},90%,60%,0)`);
        ctx.strokeStyle = g; ctx.lineWidth = rb.w*h; ctx.lineCap = "round";
        ctx.shadowBlur = 40; ctx.shadowColor = `hsla(${rb.hue},95%,60%,.5)`; ctx.stroke();
      }
      ctx.shadowBlur = 0;
      const gx = m.x*w, gy = m.y*h, gg = ctx.createRadialGradient(gx,gy,0,gx,gy,120);
      gg.addColorStop(0,"rgba(129,140,248,.18)"); gg.addColorStop(1,"rgba(0,0,0,0)");
      ctx.fillStyle = gg; ctx.fillRect(0,0,w,h);
      ctx.globalCompositeOperation = "source-over";
      raf = requestAnimationFrame(frame);
    }
    function onMove(e){ const r = cv.getBoundingClientRect(); m.tx = (e.clientX-r.left)/w; m.ty = (e.clientY-r.top)/h; }
    resize(); frame();
    addEventListener("resize", resize); cv.addEventListener("pointermove", onMove);
    return () => { cancelAnimationFrame(raf); removeEventListener("resize", resize); cv.removeEventListener("pointermove", onMove); };
  }, []);
  return <canvas ref={ref} className="w-full h-[380px] rounded-2xl bg-[#05070f]" />;
}
// HOW IT WORKS
  • 01Three ribbons are drawn as thick, soft gradient strokes; each follows a sum of sine waves so it undulates organically.
  • 02Drawing with the 'lighter' blend mode means overlapping ribbons add up into brighter aurora bands instead of covering each other.
  • 03The cursor position is eased and fed back into the wave math, bending the ribbons toward it and adding a soft radial glow.
  • 04A shadowBlur on each stroke gives the glow depth. Respects prefers-reduced-motion.
Want a custom component for your product?
Let's build it →