Animated Blobs Background
Original · freefloating blob background
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"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
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 quoteSimilar components
Browse all →
Aurora Blobs Background
OriginalAn animated hero section with soft, ever-drifting aurora gradient blobs floating behind staggered, blur-in content.

Retro Perspective Grid
OriginalA launch and waitlist section set over an infinite neon grid that scrolls into a glowing horizon with rising particles.

Conic Gradient Flow
OriginalA frosted hero backed by twin counter-rotating conic gradients, with flowing gradient text and an animated gradient-border stat card.

Spotlight Follow Background
OriginalA dot-grid section where a spring-eased spotlight follows the cursor and reveals a hidden lattice of coloured dots.

Dot Pattern Background
pattern.tsxA reusable animated background for React and Tailwind: dot pattern background.

Flickering Grid Background
grid.tsxA reusable animated background for React and Tailwind: flickering grid background.

Grid Pattern Background
pattern.tsxA reusable animated background for React and Tailwind: grid pattern background.

Retro Grid Background
grid.jsonA reusable animated background for React and Tailwind: retro grid background.

Aurora Background
Originalaurora gradient background

Mesh Gradient Background
Originalanimated mesh gradient background

Grid Background
Originalgrid line background with fade

Dots Background
Originaldotted grid background

