EmpireUI
Get Pro
← Blog8 min read#mesh gradient#css#background

CSS Mesh Gradient Background: Fluid Color Blobs Without SVG

Build fluid, multi-point mesh gradient backgrounds in pure CSS — no SVG, no canvas. Radial gradients, mix-blend-mode, and filter blur do all the heavy lifting.

fluid colorful mesh gradient blobs on dark background

What a Mesh Gradient Actually Is

You've seen them everywhere since around 2022 — those blurry, cloud-like blobs of color that bleed into each other with zero hard edges. Figma added a mesh gradient tool that year, Apple went all-in on them for macOS Ventura wallpapers, and half the SaaS landing pages on the internet started looking like a lava lamp. But most tutorials reach for SVG filters or WebGL shaders the moment complexity goes up, and that's overkill for 90% of use cases.

A mesh gradient is just multiple radial gradient 'blobs' sitting on top of each other with a heavy Gaussian blur smoothing the transitions. That's it. No magic. The 'mesh' part is a Figma abstraction — in CSS you're approximating it with stacked, blurred circles, and the result is nearly identical.

In practice, you only need three CSS properties to pull this off: radial-gradient, filter: blur(), and optionally mix-blend-mode. The browser compositing engine does the rest. Worth noting: this approach works in every major browser as of 2023, so you're not shipping anything experimental.

Honestly, the reason people don't reach for pure CSS first is that nobody ever spelled out the pattern clearly. So let's do that.

The Core Pattern: Blurred Radial Gradients

Start with a wrapper div that sets the stage — a dark or solid background color, overflow: hidden, and position: relative. Then you drop child elements inside it, each one being a radial blob with a single strong color, blurred into softness. Stack them, and the colors mix naturally where they overlap.

Here's the base structure. Every blob is an ::after-less div (or you could use pseudo-elements if you want fewer DOM nodes):

<div class="mesh-bg">
  <div class="blob blob-1"></div>
  <div class="blob blob-2"></div>
  <div class="blob blob-3"></div>
  <div class="blob blob-4"></div>
</div>
.mesh-bg {
  position: relative;
  width: 100%;
  height: 100vh;
  background: #0a0a12;
  overflow: hidden;
}

.blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(80px);
  opacity: 0.7;
}

.blob-1 {
  width: 600px;
  height: 600px;
  top: -100px;
  left: -100px;
  background: radial-gradient(circle, #7c3aed 0%, transparent 70%);
}

.blob-2 {
  width: 500px;
  height: 500px;
  top: 200px;
  right: -80px;
  background: radial-gradient(circle, #2563eb 0%, transparent 70%);
}

.blob-3 {
  width: 700px;
  height: 400px;
  bottom: -150px;
  left: 30%;
  background: radial-gradient(circle, #db2777 0%, transparent 70%);
}

.blob-4 {
  width: 400px;
  height: 400px;
  top: 50%;
  left: 25%;
  background: radial-gradient(circle, #0891b2 0%, transparent 70%);
}

That transparent 70% stop inside the radial gradient is what makes each blob fade out gently instead of cutting off. The blur(80px) on top of that blends the already-soft edges into the neighboring blobs. You can tweak the blur — 40px gives you more distinct blobs, 120px turns everything into a soft wash. Both are valid aesthetics.

mix-blend-mode: The Multiplier You're Not Using

Adding mix-blend-mode to your blobs changes everything. Without it, you get simple alpha compositing — the top blob covers whatever is behind it (reduced by opacity). With blend modes, you get additive or multiplicative color mixing that actually looks like light blending, not layers stacking.

screen is the go-to for dark backgrounds. It adds color luminance, so overlapping blobs get brighter at the intersection rather than muddier. color-dodge is more aggressive — smaller blobs can punch really bright focal points where they overlap. On light backgrounds, multiply is what you want.

.blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(80px);
  opacity: 0.8;
  mix-blend-mode: screen; /* or: color-dodge, soft-light, overlay */
}

Quick aside: mix-blend-mode creates a stacking context, which means it interacts with the parent's background color. If you slap screen on blobs and nothing happens, check that the parent has a non-transparent background set. On #0a0a12 with purple, blue, and pink blobs at 80% opacity using screen, the overlap zones will glow nearly white — which is exactly the effect you're after for a lot of hero sections.

That said, screen can wash out on light backgrounds. For a light-background mesh (think those pastel Stripe-style gradients), drop mix-blend-mode entirely, reduce blur to 60px, and crank opacity down to 0.4–0.5.

Animating the Blobs Without Killing Performance

Static mesh gradients are fine, but floating, breathing blobs are what makes a hero section feel alive. The catch: animating filter: blur() or background-position triggers repaint every frame and will absolutely tank your Lighthouse score if you're not careful. The safe path is animating only transform and opacity.

@keyframes float-1 {
  0%, 100% { transform: translate(0, 0) scale(1); }
  33%       { transform: translate(40px, -30px) scale(1.05); }
  66%       { transform: translate(-20px, 20px) scale(0.97); }
}

@keyframes float-2 {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50%       { transform: translate(-50px, 40px) scale(1.08); }
}

.blob-1 { animation: float-1 12s ease-in-out infinite; }
.blob-2 { animation: float-2 16s ease-in-out infinite; }
.blob-3 { animation: float-1 10s ease-in-out infinite reverse; }
.blob-4 { animation: float-2 14s ease-in-out infinite; }

Each blob gets a different duration and some use reverse — that way they never sync up and the motion stays organic. Keep your animation-duration above 8s. Under that threshold you start noticing the repetition. Over 20s feels too slow on large screens.

One more thing — always add will-change: transform to each blob. It signals to the browser to promote the element to its own compositor layer, which means the animation runs on the GPU and never blocks the main thread. Combine that with contain: layout style on the wrapper and you've got a mesh background that won't blow your Core Web Vitals.

.blob {
  will-change: transform;
  contain: layout style; /* on the wrapper, not the blobs */
}

.mesh-bg {
  contain: layout style;
}

Tailwind Version: Same Effect, Zero Custom CSS

If you're on a Tailwind project and you'd rather not write a separate CSS file, you can get 80% of this effect with utility classes. Tailwind v3.3+ allows arbitrary values on blur, bg-[radial-gradient(...)], and opacity, so the blobs work without touching a .css file.

export function MeshBackground({ children }) {
  return (
    <div className="relative w-full min-h-screen bg-[#0a0a12] overflow-hidden">
      {/* Blob 1 — purple */}
      <div
        className="absolute -top-24 -left-24 w-[600px] h-[600px] rounded-full opacity-70
                   blur-[80px] bg-[radial-gradient(circle,#7c3aed_0%,transparent_70%)]
                   animate-[float-1_12s_ease-in-out_infinite]"
      />
      {/* Blob 2 — blue */}
      <div
        className="absolute top-48 -right-16 w-[500px] h-[500px] rounded-full opacity-70
                   blur-[80px] bg-[radial-gradient(circle,#2563eb_0%,transparent_70%)]
                   animate-[float-2_16s_ease-in-out_infinite]"
      />
      {/* Blob 3 — pink */}
      <div
        className="absolute -bottom-36 left-[30%] w-[700px] h-[400px] rounded-full opacity-70
                   blur-[80px] bg-[radial-gradient(circle,#db2777_0%,transparent_70%)]
                   animate-[float-1_10s_ease-in-out_infinite_reverse]"
      />
      <div className="relative z-10">{children}</div>
    </div>
  );
}

You'll still need to define the float-1 and float-2 keyframes in your tailwind.config.js under theme.extend.keyframes and theme.extend.animation. That's three or four lines of config. Alternatively, just drop the keyframes in a @layer utilities block in your global CSS — Tailwind will pick them up.

Worth noting: the relative z-10 wrapper around {children} is critical. Without it, your content sits behind the blobs and you're looking at a beautiful background with invisible text. Classic mistake.

If you want pre-built gradient components that already handle stacking and z-index correctly, check out what's in the Empire UI component library — the gradient generator is also useful for generating the exact color stops you want before you hardcode them.

Accessibility and the prefers-reduced-motion Problem

Floating blobs are visually stunning but they can be genuinely uncomfortable for people with vestibular disorders. This isn't optional — prefers-reduced-motion: reduce is a WCAG 2.2 consideration and you should respect it.

The fix is two lines of CSS and you never have to think about it again:

@media (prefers-reduced-motion: reduce) {
  .blob {
    animation: none !important;
  }
}

Honestly, the static version of a mesh background is still gorgeous. Killing the animation doesn't degrade the visual — it just stops moving. Users who need reduced motion still get the full color effect, they just don't get nauseous from it. That's a win for everyone.

One more thing — if you're using this as a page background behind text, double-check your contrast ratios. The zones where blobs overlap can get very bright with mix-blend-mode: screen, and white text on a near-white glow point fails WCAG at any level. Either keep your text in a z-10 container with a subtle text shadow, or add a backdrop-filter: blur(2px) card behind text-heavy sections. The glassmorphism components pattern handles this well — it was basically designed for this scenario.

Real-World Usage: Hero Sections and App Shells

Where do these actually work best? Hero sections, authentication pages, and pricing pages — basically anywhere you need visual richness without the weight of a full image or video background. A 600px blob with 80px blur costs about the same as a single PNG in terms of rendering budget, but it's infinitely scalable and responds to dark mode with one CSS variable swap.

For dark mode, you don't need separate blobs. Just increase opacity slightly (from 0.7 to 0.85) and shift hues toward the cooler end of your palette. Purple and cyan work in both light and dark. Orange blobs get muddy on dark backgrounds, so swap them for something in the blue-violet range when you flip to dark mode.

@media (prefers-color-scheme: light) {
  .mesh-bg { background: #f8f7ff; }
  .blob { mix-blend-mode: multiply; opacity: 0.4; }
}

If you're building something more styled — cyberpunk or vaporwave — you can lean into neon colors and crank mix-blend-mode: color-dodge for that blown-out glow. For something calmer like aurora, keep your palette to two or three analogous hues with long, slow animation durations (18–24 seconds). The difference between a tacky background and a polished one is almost always restraint in the color count and speed.

The mesh gradient pattern is one of those techniques where the setup takes 20 minutes but the payoff is months of hero sections that look like you spent a week in After Effects. Worth the 80 lines of CSS.

FAQ

Does CSS mesh gradient work without JavaScript?

Yes, completely. It's pure CSS — radial gradients, blur filter, and optional keyframe animations. No JS required.

Why does my mesh gradient look flat and blurry instead of vibrant?

Two likely causes: your radial-gradient stops reach transparent too early (try extending the color stop past 50%), or you're missing mix-blend-mode on a dark background. Add mix-blend-mode: screen and it'll pop.

Will animating blobs hurt my Core Web Vitals?

Not if you only animate transform and opacity, add will-change: transform, and put contain: layout style on the wrapper. Avoid animating filter or background directly.

How many blobs is too many?

Four to six blobs covers most designs. Beyond that you're paying GPU compositing cost for colors that just muddy together anyway. Three well-placed blobs will outlook eight random ones every time.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Conic Gradient CSS: Pie Charts, Color Wheels and Angled FillsCSS Grid Pattern Background: Dot Matrix, Lines and Cross GridsCSS Gradient Animation: background-size, background-position TricksCSS Box Shadow: The Complete Guide With Live Examples