A warm, bright mesh gradient that never stops moving. Five large circles in coral, pink, violet, yellow and sky blue are blurred together into one soft field and each follows its own slow loop, so the colours keep sliding into new combinations. An SVG noise filter adds a fine film grain on top, which stops the gradient looking plastic. The hero uses a large serif headline, a short paragraph and a working email form, and reflows into a single column on phones.
import { useEffect, useId, useRef, useState } from "react";
// Each blob is one blurred circle. size is a share of the section width; path picks one of the three motion loops.
const blobs = [
{ color: "#ff5c39", size: "46%", top: "-18%", left: "-10%", path: "a", seconds: 16 },
{ color: "#ff3d81", size: "40%", top: "34%", left: "58%", path: "b", seconds: 20 },
{ color: "#7a5cff", size: "44%", top: "-24%", left: "56%", path: "c", seconds: 23 },
{ color: "#ffc233", size: "38%", top: "48%", left: "4%", path: "b", seconds: 18 },
{ color: "#27c4f5", size: "30%", top: "20%", left: "30%", path: "a", seconds: 26 },
{ color: "#ff8a4c", size: "34%", top: "40%", left: "84%", path: "b", seconds: 21 },
];
const GRAIN = 0.38; // 0 = no grain, 1 = heavy film grain
const faces = [
{ initials: "MO", bg: "bg-rose-600" },
{ initials: "TK", bg: "bg-violet-700" },
{ initials: "AD", bg: "bg-amber-700" },
{ initials: "RS", bg: "bg-sky-700" },
];
export default function GradientMeshBackgroundWithFlowingBlobsAndGrain() {
const ref = useRef<HTMLElement>(null);
const [paused, setPaused] = useState(false);
const [email, setEmail] = useState("");
const [sent, setSent] = useState(false);
const grainId = `mesh-blobs-grain-${useId().replace(/[^a-zA-Z0-9]/g, "")}`;
useEffect(() => {
const el = ref.current;
if (!el || typeof IntersectionObserver === "undefined") return;
const io = new IntersectionObserver(([entry]) => setPaused(!entry.isIntersecting));
io.observe(el);
return () => io.disconnect();
}, []);
return (
<section ref={ref} className={`mesh-blobs relative isolate overflow-hidden bg-[#fff3e4] text-stone-950 ${paused ? "mesh-blobs-paused" : ""}`}>
<style>{`
@keyframes mesh-blobs-a { 0%, 100% { transform: translate3d(0, 0, 0) scale(1); } 33% { transform: translate3d(38%, 22%, 0) scale(1.25); } 66% { transform: translate3d(12%, 46%, 0) scale(0.85); } }
@keyframes mesh-blobs-b { 0%, 100% { transform: translate3d(0, 0, 0) scale(1.1); } 33% { transform: translate3d(-42%, -28%, 0) scale(0.8); } 66% { transform: translate3d(-14%, 20%, 0) scale(1.3); } }
@keyframes mesh-blobs-c { 0%, 100% { transform: translate3d(0, 0, 0) scale(0.9) rotate(0deg); } 50% { transform: translate3d(-34%, 44%, 0) scale(1.3) rotate(40deg); } }
@keyframes mesh-blobs-rise { from { opacity: 0; transform: translate3d(0, 20px, 0); } to { opacity: 1; transform: none; } }
@keyframes mesh-blobs-bloom { from { opacity: 0; transform: scale(0.82); } to { opacity: 1; transform: none; } }
.mesh-blobs-field { animation: mesh-blobs-bloom 1.3s cubic-bezier(0.16, 1, 0.3, 1) both; }
.mesh-blobs-rise { animation: mesh-blobs-rise 0.9s cubic-bezier(0.16, 1, 0.3, 1) both; }
.mesh-blobs-paused * { animation-play-state: paused !important; }
@media (prefers-reduced-motion: reduce) { .mesh-blobs * { animation: none !important; } }
`}</style>
<div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10">
<div className="mesh-blobs-field absolute inset-0 blur-[70px] saturate-150 sm:blur-[90px]">
{blobs.map((b, i) => (
<div
key={i}
className="absolute aspect-square rounded-full opacity-90"
style={{ top: b.top, left: b.left, width: `max(${b.size}, 260px)`, background: b.color, animation: `mesh-blobs-${b.path} ${b.seconds}s ease-in-out ${-i * 3.5}s infinite` }}
/>
))}
</div>
{/* Film grain: one SVG noise filter drawn once over everything */}
<svg className="absolute inset-0 h-full w-full mix-blend-overlay" style={{ opacity: GRAIN }}>
<filter id={grainId}>
<feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="3" stitchTiles="stitch" />
<feColorMatrix type="saturate" values="0" />
</filter>
<rect width="100%" height="100%" filter={`url(#${grainId})`} />
</svg>
<svg className="absolute inset-0 h-full w-full mix-blend-multiply" style={{ opacity: GRAIN * 0.35 }}>
<rect width="100%" height="100%" filter={`url(#${grainId})`} />
</svg>
</div>
<div className="mx-auto flex min-h-[700px] max-w-6xl flex-col px-6 pb-16 pt-8">
<nav aria-label="Main" className="mesh-blobs-rise flex items-center justify-between">
<a href="#" className="flex h-10 items-center gap-2 rounded-md font-serif text-2xl font-semibold tracking-tight focus:outline-none focus-visible:ring-2 focus-visible:ring-stone-950">
<span aria-hidden="true" className="h-3 w-3 rounded-full bg-stone-950" />
Sundial
</a>
<a href="#" className="inline-flex h-10 items-center rounded-full bg-stone-950/10 px-4 text-sm font-semibold text-stone-950 backdrop-blur transition hover:bg-stone-950/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-stone-950">
Sign in
</a>
</nav>
<div className="mt-auto pt-24">
<p className="mesh-blobs-rise text-sm font-semibold uppercase tracking-[0.2em] text-stone-900" style={{ animationDelay: "0.1s" }}>
The calendar that defends your time
</p>
<h1 className="mesh-blobs-rise mt-5 max-w-4xl font-serif text-5xl font-medium leading-[0.98] tracking-tight sm:text-7xl lg:text-8xl" style={{ animationDelay: "0.2s" }}>
Fewer meetings. <em className="font-normal">Longer afternoons.</em>
</h1>
<div className="mt-10 grid items-end gap-8 lg:grid-cols-[1fr_auto]">
<div className="mesh-blobs-rise max-w-md" style={{ animationDelay: "0.32s" }}>
<p className="text-base font-medium leading-relaxed text-stone-900 sm:text-lg">
Sundial blocks out focus time before your week fills up, moves the meetings that can move, and politely declines the ones that should have been a message.
</p>
<div className="mt-6 flex items-center gap-3">
<div className="flex -space-x-2">
{faces.map((f) => (
<span key={f.initials} className={`flex h-9 w-9 items-center justify-center rounded-full text-[11px] font-semibold text-white ring-2 ring-white/80 ${f.bg}`}>
{f.initials}
</span>
))}
</div>
<p className="text-sm font-medium text-stone-900">Used by 3,100 teams who like their evenings</p>
</div>
</div>
<form
className="mesh-blobs-rise w-full lg:w-[420px]"
style={{ animationDelay: "0.44s" }}
onSubmit={(e) => {
e.preventDefault();
if (email.trim()) setSent(true);
}}
>
<div className="flex flex-col gap-2 rounded-3xl bg-white/75 p-2 shadow-2xl shadow-rose-900/20 ring-1 ring-white/80 backdrop-blur-xl sm:flex-row sm:rounded-full">
<label htmlFor="mesh-blobs-email" className="sr-only">
Work email
</label>
<input
id="mesh-blobs-email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
className="h-12 min-w-0 flex-1 rounded-full bg-transparent px-5 text-base text-stone-950 placeholder:text-stone-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-stone-950"
/>
<button type="submit" className="h-12 rounded-full bg-stone-950 px-6 text-sm font-semibold text-white transition hover:bg-stone-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-stone-950 focus-visible:ring-offset-2">
Get early access
</button>
</div>
<p className="mt-3 px-3 text-sm font-medium text-stone-900" role="status">
{sent ? "You are on the list. We will email you an invite this week." : "Free for 30 days. Works with the calendar you already use."}
</p>
</form>
</div>
</div>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/gradient-mesh-background-with-flowing-blobs-and-grain.json
There is no real mesh. Five solid circles are placed inside one wrapper that has a large blur filter, so their edges melt into each other and into the cream base. Each circle is animated with a keyframe loop that changes only `transform`, moving and scaling it. Because the blur is applied to the wrapper, overlapping circles blend like wet paint.
An inline SVG holds a filter with `feTurbulence`, which generates fractal noise, and a colour matrix that removes its colour. A rectangle the size of the section is drawn through that filter and blended over the gradient with `mix-blend-overlay`. A second, fainter pass with `mix-blend-multiply` adds a little dark speckle. Nothing is downloaded.
No, and that is deliberate. Static grain is rendered once and costs nothing afterwards. Animated noise forces a repaint of the whole section on every frame.
Yes. The blobs have a minimum size so the field stays full on small screens, the blur radius is lower below the `sm` breakpoint, and the loops pause when the section is scrolled out of view.
Only React. There is no animation library and no image file.
Related: Aurora Background with Drifting Colour Bands, Animated Grid Background with Travelling Light Beams, Starfield Background with Depth and Shooting Stars.
More animated backgrounds or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.