Web InnoventixFreeCode

Aurora Background

Original · free

aurora gradient background

byWeb InnoventixReact + Tailwind
bgxaurorabackgrounds
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/bgx-aurora.json
bgx-aurora.tsx
"use client";

import { useId, useRef, useState } from "react";
import type { KeyboardEvent } from "react";
import { motion, useReducedMotion, type Variants } from "motion/react";

type PaletteKey = "boreal" | "nebula" | "solar";

interface Palette {
  key: PaletteKey;
  label: string;
  hint: string;
  accent: string;
  ring: string;
  dot: string;
  stops: [string, string, string, string];
}

const PALETTES: Palette[] = [
  {
    key: "boreal",
    label: "Boreal",
    hint: "Green over glacier blue",
    accent: "#34d399",
    ring: "focus-visible:ring-emerald-400/70",
    dot: "bg-emerald-400",
    stops: [
      "rgba(16,185,129,0.60)",
      "rgba(56,189,248,0.52)",
      "rgba(45,212,191,0.50)",
      "rgba(110,231,183,0.55)",
    ],
  },
  {
    key: "nebula",
    label: "Nebula",
    hint: "Violet, indigo, a rose flare",
    accent: "#a78bfa",
    ring: "focus-visible:ring-violet-400/70",
    dot: "bg-violet-400",
    stops: [
      "rgba(139,92,246,0.58)",
      "rgba(99,102,241,0.52)",
      "rgba(244,63,94,0.44)",
      "rgba(56,189,248,0.46)",
    ],
  },
  {
    key: "solar",
    label: "Solar",
    hint: "Amber dawn into rose",
    accent: "#fbbf24",
    ring: "focus-visible:ring-amber-400/70",
    dot: "bg-amber-400",
    stops: [
      "rgba(251,191,36,0.56)",
      "rgba(244,63,94,0.46)",
      "rgba(251,146,60,0.50)",
      "rgba(16,185,129,0.38)",
    ],
  },
];

const GRAIN_URL =
  "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E\")";

const CSS = `
@keyframes bgxaur-drift-a {
  0%,100% { transform: translate3d(-6%, -3%, 0) scale(1.12) rotate(0deg); }
  50%     { transform: translate3d(9%, 7%, 0) scale(1.3) rotate(9deg); }
}
@keyframes bgxaur-drift-b {
  0%,100% { transform: translate3d(6%, -6%, 0) scale(1.22) rotate(0deg); }
  50%     { transform: translate3d(-8%, 4%, 0) scale(1.08) rotate(-11deg); }
}
@keyframes bgxaur-drift-c {
  0%,100% { transform: translate3d(-4%, 8%, 0) scale(1.05) rotate(0deg); }
  50%     { transform: translate3d(7%, -5%, 0) scale(1.28) rotate(7deg); }
}
@keyframes bgxaur-drift-d {
  0%,100% { transform: translate3d(5%, 5%, 0) scale(1.0) rotate(0deg); opacity: 0.85; }
  50%     { transform: translate3d(-6%, -7%, 0) scale(1.25) rotate(-6deg); opacity: 1; }
}
@keyframes bgxaur-grain-shift {
  0%   { background-position: 0 0; }
  100% { background-position: 140px 90px; }
}
.bgxaur-layer-1 { animation: bgxaur-drift-a 23s ease-in-out infinite; }
.bgxaur-layer-2 { animation: bgxaur-drift-b 29s ease-in-out infinite; }
.bgxaur-layer-3 { animation: bgxaur-drift-c 26s ease-in-out infinite; }
.bgxaur-layer-4 { animation: bgxaur-drift-d 19s ease-in-out infinite; }
.bgxaur-grain   { animation: bgxaur-grain-shift 6s steps(5) infinite; }
@media (prefers-reduced-motion: reduce) {
  .bgxaur-layer-1, .bgxaur-layer-2, .bgxaur-layer-3, .bgxaur-layer-4, .bgxaur-grain {
    animation: none !important;
  }
}
`;

const container: Variants = {
  hidden: {},
  show: { transition: { staggerChildren: 0.08, delayChildren: 0.08 } },
};
const item: Variants = {
  hidden: { opacity: 0, y: 18 },
  show: { opacity: 1, y: 0, transition: { duration: 0.7, ease: "easeOut" } },
};

export default function BgxAurora() {
  const reduce = useReducedMotion();
  const uid = useId();
  const intensityId = `${uid}-intensity`;

  const [paletteKey, setPaletteKey] = useState<PaletteKey>("boreal");
  const [intensity, setIntensity] = useState<number>(64);
  const [playing, setPlaying] = useState<boolean>(true);

  const radioRefs = useRef<Array<HTMLButtonElement | null>>([]);

  const palette = PALETTES.find((p) => p.key === paletteKey) ?? PALETTES[0];
  const strength = Math.min(1, 0.15 + (intensity / 100) * 0.9);
  const playState = playing ? "running" : "paused";

  const onRadioKeyNav = (e: KeyboardEvent<HTMLButtonElement>, idx: number) => {
    const last = PALETTES.length - 1;
    let next = idx;
    switch (e.key) {
      case "ArrowRight":
      case "ArrowDown":
        next = idx === last ? 0 : idx + 1;
        break;
      case "ArrowLeft":
      case "ArrowUp":
        next = idx === 0 ? last : idx - 1;
        break;
      case "Home":
        next = 0;
        break;
      case "End":
        next = last;
        break;
      default:
        return;
    }
    e.preventDefault();
    setPaletteKey(PALETTES[next].key);
    radioRefs.current[next]?.focus();
  };

  const layerDefs = [
    { cls: "bgxaur-layer-1", top: "-18%", left: "-12%", size: "72%", stop: palette.stops[0] },
    { cls: "bgxaur-layer-2", top: "-8%", left: "34%", size: "68%", stop: palette.stops[1] },
    { cls: "bgxaur-layer-3", top: "18%", left: "-6%", size: "62%", stop: palette.stops[2] },
    { cls: "bgxaur-layer-4", top: "6%", left: "44%", size: "54%", stop: palette.stops[3] },
  ];

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100">
      <style>{CSS}</style>

      {/* ---------- Aurora field (decorative) ---------- */}
      <div aria-hidden className="pointer-events-none absolute inset-0">
        {/* faint base wash so low intensity still has presence */}
        <div
          className="absolute inset-0"
          style={{
            background: `radial-gradient(120% 90% at 50% -10%, ${palette.accent}22, transparent 60%)`,
          }}
        />

        {/* blurred, group-blended aurora stack */}
        <div
          className="absolute inset-0 mix-blend-multiply dark:mix-blend-screen"
          style={{ opacity: strength, filter: "blur(64px) saturate(155%)" }}
        >
          {layerDefs.map((l) => (
            <div
              key={l.cls}
              className={`${l.cls} absolute rounded-full`}
              style={{
                top: l.top,
                left: l.left,
                width: l.size,
                height: l.size,
                background: `radial-gradient(closest-side, ${l.stop}, transparent 78%)`,
                willChange: "transform",
                animationPlayState: playState,
              }}
            />
          ))}
        </div>

        {/* film grain */}
        <div
          className="bgxaur-grain absolute inset-0 opacity-[0.06] mix-blend-soft-light dark:opacity-[0.1] dark:mix-blend-overlay"
          style={{
            backgroundImage: GRAIN_URL,
            backgroundSize: "140px 140px",
            animationPlayState: playState,
          }}
        />

        {/* edge vignette / bottom fade for depth */}
        <div className="absolute inset-0 bg-gradient-to-b from-slate-50/60 via-transparent to-slate-50 dark:from-slate-950/60 dark:to-slate-950" />
        <div
          className="absolute inset-0"
          style={{
            background:
              "radial-gradient(115% 80% at 50% 8%, transparent 45%, rgba(2,6,23,0.10) 100%)",
          }}
        />
      </div>

      {/* ---------- Content ---------- */}
      <motion.div
        variants={container}
        initial={reduce ? false : "hidden"}
        animate="show"
        className="relative mx-auto flex min-h-[620px] max-w-5xl flex-col justify-center px-6 py-24 sm:px-8 sm:py-32"
      >
        <motion.p
          variants={item}
          className="mb-6 inline-flex items-center gap-2 self-start rounded-full border border-slate-300/70 bg-white/60 px-3 py-1 text-xs font-medium tracking-wide text-slate-600 backdrop-blur-md dark:border-white/15 dark:bg-white/5 dark:text-slate-300"
        >
          <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" aria-hidden>
            <path
              d="M12 2c.5 3.4 2.1 5 5.5 5.5C14.1 8 12.5 9.6 12 13c-.5-3.4-2.1-5-5.5-5.5C9.9 7 11.5 5.4 12 2Z"
              fill="currentColor"
            />
            <path
              d="M19 13c.3 1.9 1.1 2.7 3 3-1.9.3-2.7 1.1-3 3-.3-1.9-1.1-2.7-3-3 1.9-.3 2.7-1.1 3-3Z"
              fill="currentColor"
              opacity="0.7"
            />
          </svg>
          FreeCode / Backgrounds
        </motion.p>

        <motion.h1
          variants={item}
          className="max-w-3xl text-balance text-5xl font-semibold leading-[1.02] tracking-tight sm:text-6xl md:text-7xl"
        >
          Aurora, painted live in the browser.
        </motion.h1>

        <motion.p
          variants={item}
          className="mt-6 max-w-xl text-pretty text-lg leading-relaxed text-slate-600 dark:text-slate-300"
        >
          A drifting gradient field built from four layers of radial light — no
          canvas, no WebGL, no images. Pick a palette, dial the intensity, and
          drop it behind any hero, pricing table, or sign-up card.
        </motion.p>

        {/* feature chips */}
        <motion.ul
          variants={item}
          className="mt-6 flex flex-wrap gap-2 text-xs font-medium text-slate-500 dark:text-slate-400"
        >
          {["No <canvas>", "No WebGL", "GPU transforms only", "Light + dark"].map(
            (chip) => (
              <li
                key={chip}
                className="rounded-full border border-slate-200 bg-white/50 px-2.5 py-1 backdrop-blur-sm dark:border-white/10 dark:bg-white/5"
              >
                {chip}
              </li>
            )
          )}
        </motion.ul>

        {/* ---------- Controls ---------- */}
        <motion.div
          variants={item}
          role="group"
          aria-label="Aurora controls"
          className="mt-10 w-full max-w-xl rounded-3xl border border-slate-200/80 bg-white/65 p-5 shadow-xl shadow-slate-900/5 backdrop-blur-xl dark:border-white/10 dark:bg-white/[0.06] dark:shadow-black/30 sm:p-6"
        >
          {/* palette radiogroup */}
          <div className="flex flex-col gap-3">
            <span className="text-sm font-semibold text-slate-800 dark:text-slate-100">
              Palette
            </span>
            <div
              role="radiogroup"
              aria-label="Aurora palette"
              className="inline-flex flex-wrap gap-1.5 rounded-2xl bg-slate-100/80 p-1.5 dark:bg-white/5"
            >
              {PALETTES.map((p, i) => {
                const selected = p.key === paletteKey;
                return (
                  <button
                    key={p.key}
                    type="button"
                    role="radio"
                    aria-checked={selected}
                    tabIndex={selected ? 0 : -1}
                    ref={(el) => {
                      radioRefs.current[i] = el;
                    }}
                    onClick={() => setPaletteKey(p.key)}
                    onKeyDown={(e) => onRadioKeyNav(e, i)}
                    title={p.hint}
                    className={[
                      "flex items-center gap-2 rounded-xl px-3.5 py-2 text-sm font-medium outline-none transition",
                      "focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent",
                      palette.ring,
                      selected
                        ? "bg-white text-slate-900 shadow-sm dark:bg-white/15 dark:text-white"
                        : "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200",
                    ].join(" ")}
                  >
                    <span
                      className={`h-2.5 w-2.5 rounded-full ${p.dot} ${
                        selected ? "" : "opacity-60"
                      }`}
                    />
                    {p.label}
                  </button>
                );
              })}
            </div>
            <p className="text-xs text-slate-500 dark:text-slate-400">
              {palette.hint}
            </p>
          </div>

          {/* intensity slider */}
          <div className="mt-6 flex flex-col gap-2">
            <div className="flex items-center justify-between">
              <label
                htmlFor={intensityId}
                className="text-sm font-semibold text-slate-800 dark:text-slate-100"
              >
                Intensity
              </label>
              <span className="tabular-nums text-sm font-medium text-slate-500 dark:text-slate-400">
                {intensity}%
              </span>
            </div>
            <input
              id={intensityId}
              type="range"
              min={0}
              max={100}
              step={1}
              value={intensity}
              onChange={(e) => setIntensity(Number(e.currentTarget.value))}
              aria-valuetext={`${intensity} percent`}
              style={{ accentColor: palette.accent }}
              className={`h-2 w-full cursor-pointer appearance-none rounded-full bg-slate-200 outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent dark:bg-white/10 ${palette.ring}`}
            />
          </div>

          {/* motion switch */}
          <div className="mt-6 flex items-center justify-between">
            <div className="flex flex-col">
              <span className="text-sm font-semibold text-slate-800 dark:text-slate-100">
                Motion
              </span>
              <span className="text-xs text-slate-500 dark:text-slate-400">
                {reduce
                  ? "Paused — respecting your reduced-motion setting"
                  : playing
                    ? "Layers are drifting"
                    : "Layers are frozen"}
              </span>
            </div>
            <button
              type="button"
              role="switch"
              aria-checked={playing}
              aria-label="Toggle aurora motion"
              onClick={() => setPlaying((v) => !v)}
              className={`relative inline-flex h-7 w-12 shrink-0 items-center rounded-full outline-none transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent ${palette.ring} ${
                playing ? "" : "bg-slate-300 dark:bg-white/15"
              }`}
              style={playing ? { backgroundColor: palette.accent } : undefined}
            >
              <span
                className={`inline-block h-5 w-5 transform rounded-full bg-white shadow-sm transition-transform ${
                  playing ? "translate-x-6" : "translate-x-1"
                }`}
              />
            </button>
          </div>

          {/* live status readout */}
          <p
            aria-live="polite"
            className="mt-5 border-t border-slate-200/70 pt-4 text-xs font-medium tabular-nums text-slate-500 dark:border-white/10 dark:text-slate-400"
          >
            {palette.label} &middot; Intensity {intensity}% &middot; Motion{" "}
            {playing && !reduce ? "on" : "off"}
          </p>
        </motion.div>
      </motion.div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →