Web InnoventixFreeCode

Animated Blobs Background

Original · free

floating blob background

byWeb InnoventixReact + Tailwind
bgxanimatedblobsbackgrounds
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-animated-blobs.json
bgx-animated-blobs.tsx
"use client";

import { useState } from "react";
import { motion, useReducedMotion } from "motion/react";

type PaletteKey = "aurora" | "meadow" | "ember";
type SpeedKey = "calm" | "flow" | "rush";

interface BlobConfig {
  anim: string;
  pos: string;
  size: string;
  base: number;
  delay: number;
  tone: number;
}

const PALETTES: Record<
  PaletteKey,
  { label: string; swatch: string; tones: string[] }
> = {
  aurora: {
    label: "Aurora",
    swatch: "from-indigo-400 to-violet-400",
    tones: [
      "bg-indigo-400 dark:bg-indigo-500",
      "bg-violet-400 dark:bg-violet-500",
      "bg-sky-400 dark:bg-sky-500",
      "bg-indigo-300 dark:bg-indigo-400",
      "bg-violet-300 dark:bg-violet-600",
      "bg-sky-300 dark:bg-sky-600",
    ],
  },
  meadow: {
    label: "Meadow",
    swatch: "from-emerald-400 to-sky-400",
    tones: [
      "bg-emerald-400 dark:bg-emerald-500",
      "bg-sky-400 dark:bg-sky-500",
      "bg-emerald-300 dark:bg-emerald-600",
      "bg-sky-300 dark:bg-sky-600",
      "bg-emerald-400 dark:bg-emerald-400",
      "bg-sky-400 dark:bg-sky-400",
    ],
  },
  ember: {
    label: "Ember",
    swatch: "from-rose-400 to-amber-400",
    tones: [
      "bg-rose-400 dark:bg-rose-500",
      "bg-amber-400 dark:bg-amber-500",
      "bg-rose-300 dark:bg-rose-600",
      "bg-amber-300 dark:bg-amber-600",
      "bg-violet-400 dark:bg-violet-500",
      "bg-rose-400 dark:bg-rose-400",
    ],
  },
};

const SPEEDS: Record<SpeedKey, { label: string; factor: number }> = {
  calm: { label: "Calm", factor: 1.6 },
  flow: { label: "Flow", factor: 1 },
  rush: { label: "Rush", factor: 0.6 },
};

const BLOBS: BlobConfig[] = [
  { anim: "bgxblob-d1", pos: "top-[-8rem] left-[-6rem]", size: "h-[30rem] w-[30rem]", base: 17, delay: 0, tone: 0 },
  { anim: "bgxblob-d2", pos: "top-[-4rem] right-[-8rem]", size: "h-[34rem] w-[34rem]", base: 21, delay: 1.4, tone: 1 },
  { anim: "bgxblob-d3", pos: "bottom-[-10rem] left-[10%]", size: "h-[28rem] w-[28rem]", base: 19, delay: 0.8, tone: 2 },
  { anim: "bgxblob-d4", pos: "bottom-[-6rem] right-[6%]", size: "h-[26rem] w-[26rem]", base: 23, delay: 2.1, tone: 3 },
  { anim: "bgxblob-d5", pos: "top-[30%] left-[38%]", size: "h-[22rem] w-[22rem]", base: 15, delay: 0.4, tone: 4 },
  { anim: "bgxblob-d6", pos: "top-[12%] right-[26%]", size: "h-[20rem] w-[20rem]", base: 25, delay: 1.1, tone: 5 },
];

const DEFAULTS = {
  palette: "aurora" as PaletteKey,
  speed: "flow" as SpeedKey,
  softness: 70,
  glow: 72,
};

export default function BgxAnimatedBlobs() {
  const prefersReduced = useReducedMotion();

  const [palette, setPalette] = useState<PaletteKey>(DEFAULTS.palette);
  const [speed, setSpeed] = useState<SpeedKey>(DEFAULTS.speed);
  const [softness, setSoftness] = useState<number>(DEFAULTS.softness);
  const [glow, setGlow] = useState<number>(DEFAULTS.glow);
  const [playing, setPlaying] = useState<boolean>(true);

  const animating = playing && !prefersReduced;
  const tones = PALETTES[palette].tones;
  const factor = SPEEDS[speed].factor;

  const reset = () => {
    setPalette(DEFAULTS.palette);
    setSpeed(DEFAULTS.speed);
    setSoftness(DEFAULTS.softness);
    setGlow(DEFAULTS.glow);
    setPlaying(true);
  };

  const ring =
    "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950";

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-6 py-24 text-slate-900 sm:py-32 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes bgxblob-d1 { 0% { transform: translate3d(0,0,0) scale(1); } 100% { transform: translate3d(6%, 8%, 0) scale(1.12); } }
        @keyframes bgxblob-d2 { 0% { transform: translate3d(0,0,0) scale(1.05); } 100% { transform: translate3d(-7%, -6%, 0) scale(0.95); } }
        @keyframes bgxblob-d3 { 0% { transform: translate3d(0,0,0) scale(0.95); } 100% { transform: translate3d(5%, -9%, 0) scale(1.1); } }
        @keyframes bgxblob-d4 { 0% { transform: translate3d(0,0,0) scale(1); } 100% { transform: translate3d(-6%, 7%, 0) scale(1.08); } }
        @keyframes bgxblob-d5 { 0% { transform: translate3d(0,0,0) scale(1.1); } 100% { transform: translate3d(8%, 4%, 0) scale(0.96); } }
        @keyframes bgxblob-d6 { 0% { transform: translate3d(0,0,0) scale(1); } 100% { transform: translate3d(-5%, -8%, 0) scale(1.14); } }
        .bgxblob {
          animation-iteration-count: infinite;
          animation-direction: alternate;
          animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
          will-change: transform;
        }
        .bgxrange::-webkit-slider-thumb {
          -webkit-appearance: none;
          appearance: none;
          height: 1.125rem;
          width: 1.125rem;
          border-radius: 9999px;
          background: #6366f1;
          border: 2px solid #ffffff;
          box-shadow: 0 1px 4px rgba(15, 23, 42, 0.35);
          cursor: pointer;
        }
        .bgxrange::-moz-range-thumb {
          height: 1.125rem;
          width: 1.125rem;
          border-radius: 9999px;
          background: #6366f1;
          border: 2px solid #ffffff;
          box-shadow: 0 1px 4px rgba(15, 23, 42, 0.35);
          cursor: pointer;
        }
        @media (prefers-reduced-motion: reduce) {
          .bgxblob { animation: none !important; transform: none !important; }
        }
      `}</style>

      {/* Animated blob field */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
        {BLOBS.map((b) => (
          <span
            key={b.anim}
            className={`bgxblob absolute rounded-full mix-blend-multiply dark:mix-blend-screen ${b.pos} ${b.size} ${tones[b.tone]}`}
            style={{
              animationName: b.anim,
              animationDuration: `${(b.base * factor).toFixed(2)}s`,
              animationDelay: `${b.delay}s`,
              animationPlayState: animating ? "running" : "paused",
              filter: `blur(${softness}px)`,
              opacity: glow / 100,
            }}
          />
        ))}
        {/* faint grid to read as a real surface */}
        <div
          className="absolute inset-0 opacity-[0.06] dark:opacity-[0.09]"
          style={{
            backgroundImage:
              "linear-gradient(to right, currentColor 1px, transparent 1px), linear-gradient(to bottom, currentColor 1px, transparent 1px)",
            backgroundSize: "48px 48px",
          }}
        />
      </div>

      <motion.div
        initial={prefersReduced ? false : { opacity: 0, y: 18 }}
        animate={prefersReduced ? undefined : { opacity: 1, y: 0 }}
        transition={{ duration: 0.6, ease: "easeOut" }}
        className="relative mx-auto grid max-w-6xl grid-cols-1 items-start gap-8 lg:grid-cols-[1.1fr_0.9fr]"
      >
        {/* Hero copy on the surface */}
        <div className="rounded-3xl border border-slate-200/70 bg-white/70 p-8 shadow-xl shadow-slate-900/5 backdrop-blur-xl sm:p-10 dark:border-white/10 dark:bg-slate-900/50 dark:shadow-black/30">
          <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/60 px-3 py-1 text-xs font-semibold uppercase tracking-widest text-slate-500 dark:border-white/10 dark:bg-white/5 dark:text-slate-400">
            <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" aria-hidden="true" />
            Ambient background
          </span>
          <h2 className="mt-6 text-4xl font-semibold tracking-tight text-slate-900 sm:text-5xl dark:text-white">
            Set a mood without stealing the scene
          </h2>
          <p className="mt-5 max-w-xl text-base leading-relaxed text-slate-600 sm:text-lg dark:text-slate-300">
            Six soft gradient orbs drift on their own timeline behind your
            content. Tune the palette, pace, softness, and glow until it reads as
            calm, not chaotic, then drop your hero, pricing table, or sign-up form
            straight on top.
          </p>

          <dl className="mt-8 grid grid-cols-2 gap-4 sm:grid-cols-3">
            {[
              { k: "GPU-friendly", v: "transform-only keyframes" },
              { k: "A11y-first", v: "honours reduced motion" },
              { k: "Zero deps", v: "React + motion only" },
            ].map((item) => (
              <div
                key={item.k}
                className="rounded-xl border border-slate-200/70 bg-white/50 p-4 dark:border-white/10 dark:bg-white/5"
              >
                <dt className="text-sm font-semibold text-slate-900 dark:text-white">
                  {item.k}
                </dt>
                <dd className="mt-1 text-xs text-slate-500 dark:text-slate-400">
                  {item.v}
                </dd>
              </div>
            ))}
          </dl>
        </div>

        {/* Live control panel */}
        <div className="rounded-3xl border border-slate-200/70 bg-white/80 p-6 shadow-xl shadow-slate-900/5 backdrop-blur-xl sm:p-7 dark:border-white/10 dark:bg-slate-900/60 dark:shadow-black/30">
          <div className="flex items-center justify-between gap-4">
            <div>
              <h3 className="text-sm font-semibold text-slate-900 dark:text-white">
                Live controls
              </h3>
              <p className="text-xs text-slate-500 dark:text-slate-400">
                Every change updates the field behind you.
              </p>
            </div>
            <button
              type="button"
              onClick={() => setPlaying((p) => !p)}
              disabled={!!prefersReduced}
              aria-pressed={animating}
              aria-label={
                prefersReduced
                  ? "Motion disabled by system preference"
                  : animating
                    ? "Pause blob animation"
                    : "Play blob animation"
              }
              className={`inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-slate-300 bg-white text-slate-700 transition hover:border-indigo-400 hover:text-indigo-600 disabled:cursor-not-allowed disabled:opacity-40 dark:border-white/15 dark:bg-white/5 dark:text-slate-200 dark:hover:border-indigo-400 dark:hover:text-indigo-300 ${ring}`}
            >
              {animating ? (
                <svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor" aria-hidden="true">
                  <rect x="6" y="5" width="4" height="14" rx="1" />
                  <rect x="14" y="5" width="4" height="14" rx="1" />
                </svg>
              ) : (
                <svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor" aria-hidden="true">
                  <path d="M8 5.14v13.72a1 1 0 0 0 1.5.86l11-6.86a1 1 0 0 0 0-1.72l-11-6.86A1 1 0 0 0 8 5.14Z" />
                </svg>
              )}
            </button>
          </div>

          {/* Palette */}
          <fieldset className="mt-6">
            <legend className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
              Palette
            </legend>
            <div className="mt-3 grid grid-cols-3 gap-2">
              {(Object.keys(PALETTES) as PaletteKey[]).map((key) => (
                <label key={key} className="cursor-pointer">
                  <input
                    type="radio"
                    name="bgxblob-palette"
                    value={key}
                    checked={palette === key}
                    onChange={() => setPalette(key)}
                    className="peer sr-only"
                  />
                  <span
                    className={`flex flex-col items-center gap-2 rounded-xl border border-slate-200 bg-white/60 px-2 py-3 text-xs font-medium text-slate-600 transition peer-checked:border-indigo-500 peer-checked:text-indigo-700 peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:peer-checked:border-indigo-400 dark:peer-checked:text-indigo-300 dark:peer-focus-visible:ring-offset-slate-900`}
                  >
                    <span
                      className={`h-6 w-6 rounded-full bg-gradient-to-br ${PALETTES[key].swatch}`}
                      aria-hidden="true"
                    />
                    {PALETTES[key].label}
                  </span>
                </label>
              ))}
            </div>
          </fieldset>

          {/* Speed */}
          <fieldset className="mt-5">
            <legend className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
              Pace
            </legend>
            <div className="mt-3 grid grid-cols-3 gap-2">
              {(Object.keys(SPEEDS) as SpeedKey[]).map((key) => (
                <label key={key} className="cursor-pointer">
                  <input
                    type="radio"
                    name="bgxblob-speed"
                    value={key}
                    checked={speed === key}
                    onChange={() => setSpeed(key)}
                    className="peer sr-only"
                  />
                  <span
                    className={`flex items-center justify-center rounded-xl border border-slate-200 bg-white/60 px-2 py-2.5 text-xs font-medium text-slate-600 transition peer-checked:border-indigo-500 peer-checked:bg-indigo-500/10 peer-checked:text-indigo-700 peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:peer-checked:border-indigo-400 dark:peer-checked:text-indigo-300 dark:peer-focus-visible:ring-offset-slate-900`}
                  >
                    {SPEEDS[key].label}
                  </span>
                </label>
              ))}
            </div>
          </fieldset>

          {/* Softness slider */}
          <div className="mt-6">
            <div className="flex items-center justify-between">
              <label
                htmlFor="bgxblob-softness"
                className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400"
              >
                Softness
              </label>
              <span className="text-xs font-semibold tabular-nums text-slate-700 dark:text-slate-300">
                {softness}px
              </span>
            </div>
            <input
              id="bgxblob-softness"
              type="range"
              min={30}
              max={110}
              step={1}
              value={softness}
              onChange={(e) => setSoftness(Number(e.target.value))}
              aria-valuetext={`${softness} pixels of blur`}
              className={`bgxrange mt-3 h-2 w-full cursor-pointer appearance-none rounded-full bg-slate-200 dark:bg-slate-700 ${ring}`}
            />
          </div>

          {/* Glow slider */}
          <div className="mt-5">
            <div className="flex items-center justify-between">
              <label
                htmlFor="bgxblob-glow"
                className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400"
              >
                Glow
              </label>
              <span className="text-xs font-semibold tabular-nums text-slate-700 dark:text-slate-300">
                {glow}%
              </span>
            </div>
            <input
              id="bgxblob-glow"
              type="range"
              min={25}
              max={100}
              step={1}
              value={glow}
              onChange={(e) => setGlow(Number(e.target.value))}
              aria-valuetext={`${glow} percent opacity`}
              className={`bgxrange mt-3 h-2 w-full cursor-pointer appearance-none rounded-full bg-slate-200 dark:bg-slate-700 ${ring}`}
            />
          </div>

          <div className="mt-6 flex items-center justify-between gap-3 border-t border-slate-200/70 pt-5 dark:border-white/10">
            <p className="text-xs text-slate-500 dark:text-slate-400">
              {prefersReduced
                ? "Motion is off to match your system setting."
                : animating
                  ? "Field is drifting."
                  : "Field is paused."}
            </p>
            <button
              type="button"
              onClick={reset}
              className={`inline-flex items-center gap-1.5 rounded-full border border-slate-300 bg-white px-3.5 py-1.5 text-xs font-semibold text-slate-700 transition hover:border-indigo-400 hover:text-indigo-600 dark:border-white/15 dark:bg-white/5 dark:text-slate-200 dark:hover:border-indigo-400 dark:hover:text-indigo-300 ${ring}`}
            >
              <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
                <path d="M3 3v5h5" />
              </svg>
              Reset
            </button>
          </div>
        </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 →