# Aurora Veil (`aurora-veil`)

> An aurora borealis background — noise-driven light curtains ripple and fold across the sky over twinkling stars; on light themes the same curtains render as soft pigment veils.

- **Docs:** https://www.mellowui.com/components/aurora-veil
- **Markdown:** https://www.mellowui.com/components/aurora-veil.md
- **Registry:** https://www.mellowui.com/r/aurora-veil.json
- **Tool prompt:** https://www.mellowui.com/api/prompt/aurora-veil
- **Categories:** background, canvas, animation
- **Dependencies:** none

## AI prompt

Add an AuroraVeil component from the mellow library — an aurora borealis background — noise-driven light curtains ripple and fold across the sky over twinkling stars; on light themes the same curtains render as soft pigment veils. Key props: `colors`, `speed`, `intensity`, `stars`, `children`. Copy the file into your project; it is self-contained and respects prefers-reduced-motion.

## Install

```bash
npx shadcn@latest add https://mellowui.com/r/aurora-veil.json
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `colors` | `string[]` | — | Curtain colours as "R, G, B" triplets, one per band. |
| `speed` | `number` | `1` | Animation speed multiplier. |
| `intensity` | `number` | `1` | Overall brightness of the curtains, 0–2. |
| `stars` | `boolean` | `true` | Twinkling stars behind the aurora (dark themes only). |
| `className` | `string` | — | Additional CSS classes. |
| `children` | `ReactNode` | — | Content rendered inside the component. |

## Design notes

- Client component (`"use client"`) — required for animation and refs.
- Uses Mellow design tokens: `--ink`, `--ink-rgb`, `--background`, `--background-rgb`, `--rule`, `--font-sans`, `--font-serif`, `--font-mono`. Tokens auto-flip between light and dark themes — never hardcode colors.
- Respects `prefers-reduced-motion: reduce` where applicable.
- Self-contained — no shared utilities. Copy-paste, not a package.

## Source

```tsx
"use client";

import React, { useEffect, useRef, useState } from "react";

function useInkRgb(): string {
  const [rgb, setRgb] = useState<string>("244, 241, 236");
  useEffect(() => {
    const read = () => {
      const v = getComputedStyle(document.documentElement)
        .getPropertyValue("--ink-rgb")
        .trim();
      if (v) setRgb(v);
    };
    read();
    const obs = new MutationObserver(read);
    obs.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class", "style", "data-theme"],
    });
    return () => obs.disconnect();
  }, []);
  return rgb;
}

/** True when the page background is light — re-checks on theme change. */
function useIsLight(): boolean {
  const [light, setLight] = useState(false);
  useEffect(() => {
    const read = () => {
      const raw = getComputedStyle(document.documentElement)
        .getPropertyValue("--background-rgb")
        .split(",")
        .map((n) => parseFloat(n));
      if (raw.length >= 3 && raw.every((n) => !isNaN(n))) {
        setLight((raw[0] * 0.299 + raw[1] * 0.587 + raw[2] * 0.114) / 255 > 0.5);
      }
    };
    read();
    const obs = new MutationObserver(read);
    obs.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class", "style", "data-theme"],
    });
    return () => obs.disconnect();
  }, []);
  return light;
}

function hash(x: number, y: number): number {
  const n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;
  return n - Math.floor(n);
}

function vnoise(x: number, y: number): number {
  const ix = Math.floor(x), iy = Math.floor(y);
  const fx = x - ix, fy = y - iy;
  const ux = fx * fx * (3 - 2 * fx);
  const uy = fy * fy * (3 - 2 * fy);
  const ab = hash(ix, iy) + (hash(ix + 1, iy) - hash(ix, iy)) * ux;
  const cd = hash(ix, iy + 1) + (hash(ix + 1, iy + 1) - hash(ix, iy + 1)) * ux;
  return ab + (cd - ab) * uy;
}

export interface AuroraVeilProps {
  /** Curtain colours as "R, G, B" triplets, one per band. */
  colors?: string[];
  /** Animation speed multiplier. */
  speed?: number;
  /** Overall brightness of the curtains, 0–2. */
  intensity?: number;
  /** Twinkling stars behind the aurora (dark themes only). */
  stars?: boolean;
  className?: string;
  style?: React.CSSProperties;
  children?: React.ReactNode;
}

// luminous ribbons for night skies
const NIGHT_COLORS = ["120, 235, 175", "95, 185, 235", "175, 130, 240"];
// pigment veils for paper
const DAY_COLORS = ["20, 130, 95", "30, 100, 160", "110, 60, 180"];

/**
 * An aurora borealis background — noise-driven light curtains ripple and
 * fold across the sky over twinkling stars. On light themes the same
 * curtains render as soft pigment veils.
 */
export function AuroraVeil({
  colors,
  speed = 1,
  intensity = 1,
  stars = true,
  className,
  style,
  children,
}: AuroraVeilProps) {
  const inkRgb = useInkRgb();
  const isLight = useIsLight();
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const sizeRef = useRef({ w: 0, h: 0 });

  useEffect(() => {
    if (!canvasRef.current) return;
    const canvas: HTMLCanvasElement = canvasRef.current;
    const ctxOrNull = canvas.getContext("2d");
    if (!ctxOrNull) return;
    const ctx: CanvasRenderingContext2D = ctxOrNull;

    const buf = document.createElement("canvas");
    const bctxOrNull = buf.getContext("2d");
    if (!bctxOrNull) return;
    const bctx: CanvasRenderingContext2D = bctxOrNull;

    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const palette = colors ?? (isLight ? DAY_COLORS : NIGHT_COLORS);
    const alphaScale = (isLight ? 0.2 : 0.34) * intensity;

    let rafId = 0;
    let t = 0.7;
    let last = 0;

    function resize() {
      const dpr = Math.min(window.devicePixelRatio, 2);
      const rect = canvas.getBoundingClientRect();
      sizeRef.current = { w: rect.width, h: rect.height };
      canvas.width = rect.width * dpr;
      canvas.height = rect.height * dpr;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      buf.width = Math.max(2, Math.ceil(rect.width / 4));
      buf.height = Math.max(2, Math.ceil(rect.height / 4));
    }

    function frame() {
      const { w, h } = sizeRef.current;
      if (w === 0 || h === 0) return;
      const bw = buf.width;
      const bh = buf.height;

      bctx.setTransform(1, 0, 0, 1, 0, 0);
      bctx.globalCompositeOperation = "source-over";
      bctx.clearRect(0, 0, bw, bh);
      bctx.globalCompositeOperation = isLight ? "source-over" : "lighter";

      const step = 4;
      for (let l = 0; l < palette.length; l++) {
        const seed = l * 17.7;
        const drift = t * (0.6 + l * 0.25);
        // top edge of the curtain
        const top: number[] = [];
        const len: number[] = [];
        const n = Math.ceil(bw / step) + 1;
        for (let i = 0; i < n; i++) {
          const x = (i * step) / bw;
          const band =
            vnoise(x * 2.2 + seed, drift) * 0.55 +
            vnoise(x * 5.5 + seed + 9, drift * 1.6) * 0.2;
          top[i] = bh * (0.05 + l * 0.14) + band * bh * 0.36;
          len[i] = bh * (0.28 + vnoise(x * 3.1 + seed + 4, drift * 1.2) * 0.42);
        }

        // three offset passes fake the vertical ray falloff
        for (let pass = 0; pass < 3; pass++) {
          const pf = 1 - pass * 0.33;
          bctx.beginPath();
          bctx.moveTo(0, top[0] + pass * 4);
          for (let i = 1; i < n; i++) {
            const px = i * step;
            const mx = px - step / 2;
            bctx.quadraticCurveTo(
              mx,
              top[i - 1] + pass * 4,
              px,
              top[i] + pass * 4
            );
          }
          for (let i = n - 1; i >= 0; i--) {
            bctx.lineTo(i * step, top[i] + len[i] * pf + pass * 10);
          }
          bctx.closePath();

          const g = bctx.createLinearGradient(0, bh * (0.02 + l * 0.12), 0, bh);
          const a = alphaScale * (0.5 - pass * 0.14);
          g.addColorStop(0, `rgba(${palette[l]}, 0)`);
          g.addColorStop(0.16, `rgba(${palette[l]}, ${a.toFixed(3)})`);
          g.addColorStop(0.6, `rgba(${palette[l]}, ${(a * 0.25).toFixed(3)})`);
          g.addColorStop(1, `rgba(${palette[l]}, 0)`);
          bctx.fillStyle = g;
          bctx.fill();
        }
      }

      ctx.clearRect(0, 0, w, h);

      if (stars && !isLight) {
        for (let i = 0; i < 70; i++) {
          const sxr = hash(i, 1.3);
          const syr = hash(i, 7.9);
          const tw = 0.15 + vnoise(i * 3.3, t * 2.4) * 0.55;
          ctx.fillStyle = `rgba(${inkRgb}, ${tw.toFixed(3)})`;
          const r = 0.5 + hash(i, 11.1) * 1.1;
          ctx.fillRect(sxr * w, syr * h * 0.85, r, r);
        }
      }

      // blurred upscale — the cheap glow
      ctx.imageSmoothingEnabled = true;
      ctx.filter = "blur(10px)";
      ctx.drawImage(buf, -12, -12, w + 24, h + 24);
      ctx.filter = "none";
    }

    function loop(now: number) {
      const dt = Math.min((now - last) / 1000, 1 / 20);
      last = now;
      t += dt * 0.16 * speed;
      frame();
      rafId = requestAnimationFrame(loop);
    }

    const ro = new ResizeObserver(() => {
      resize();
      if (reduced) frame();
    });
    ro.observe(canvas);
    resize();

    if (reduced) {
      frame();
    } else {
      last = performance.now();
      rafId = requestAnimationFrame(loop);
    }

    return () => {
      cancelAnimationFrame(rafId);
      ro.disconnect();
    };
  }, [colors, speed, intensity, stars, inkRgb, isLight]);

  return (
    <div
      className={["relative overflow-hidden", className].filter(Boolean).join(" ")}
      style={style}
    >
      <canvas
        ref={canvasRef}
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 h-full w-full"
      />
      {children && <div className="absolute inset-0 z-[1]">{children}</div>}
    </div>
  );
}

export default AuroraVeil;

```

## Demo

```tsx
"use client";

import React from "react";
import { AuroraVeil } from "../mellow/aurora-veil";

export default function AuroraVeilDemo() {
  return (
    <AuroraVeil className="h-full w-full">
      <div className="flex h-full flex-col items-center justify-center gap-4 text-center">
        <p className="[font-family:var(--font-mono)] text-[0.625rem] font-medium tracking-[0.24em] text-[rgba(var(--ink-rgb),0.55)] uppercase">
          69.6° N, 18.9° E — Tromsø
        </p>
        <h2 className="max-w-md [font-family:var(--font-serif)] text-4xl leading-tight text-[var(--ink)] italic">
          The sky, rehearsing
        </h2>
        <p className="max-w-sm text-[0.875rem] leading-relaxed text-[rgba(var(--ink-rgb),0.6)]">
          Noise-driven curtains of light fold and ripple behind your content,
          over a field of twinkling stars.
        </p>
      </div>
    </AuroraVeil>
  );
}

```
