---
name: mesh-gradient
description: Use when you want "Premium, trendy, creative" - Uses soft blended color blobs to create a rich abstract background.
---

# Mesh Gradient

> **Category:** Background  -  **Personality:** Premium, trendy, creative
>
> **Best use:** SaaS, AI, design brands
>
> **Ratings:** Professional 5/5 - Casual 4/5 - Exotic 4/5 - Visual impact 5/5 - Difficulty 3/5 - Performance cost 3/5

## What it is

A **mesh gradient** is a rich, abstract backdrop built from several soft colour "blobs" — each a `radial-gradient` that fades to transparent — stacked in one box so they bleed into each other. The result reads as a smooth, multi-point colour field (the look SwiftUI's `MeshGradient`, Stripe and Linear made popular) rather than a single two-stop gradient. It feels premium, trendy and creative, which is why SaaS, AI and design brands lean on it for heroes and section backgrounds. This build adds two signatures: a **live palette switcher** (the mesh recolours with a smooth crossfade) and a **gentle drift** so the field is always quietly alive.

## Dependencies / CDN

**None — pure CSS** (the drift and recolour are CSS; JS only wires up the palette buttons and an optional parallax). The demo loads two display/body fonts, which are optional:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=Sora:wght@500;600;700;800&display=swap" rel="stylesheet">
```

## HTML

Two drifting mesh layers behind the content; the palette buttons just toggle a class on the stage.

```html
<div class="mg-stage" id="mgStage">           <!-- the only element that holds the palette vars -->
  <div class="mg-mesh" id="mgMesh" aria-hidden="true">
    <div class="mg-layer mg-a"></div>          <!-- 6-blob stack, drifts one way -->
    <div class="mg-layer mg-b"></div>          <!-- 4-blob stack, screen-blended, drifts the other -->
  </div>

  <div class="mg-content">
    <h1 class="mg-h">Backgrounds that feel like <em>your brand</em>.</h1>

    <div class="mg-swatches" id="mgSwatches" role="group" aria-label="Mesh colour palette">
      <button class="mg-sw sw-nebula"   data-pal="nebula"   aria-pressed="true"></button>
      <button class="mg-sw sw-solstice" data-pal="solstice" aria-pressed="false"></button>
      <button class="mg-sw sw-verdant"  data-pal="verdant"  aria-pressed="false"></button>
      <button class="mg-sw sw-orchid"   data-pal="orchid"   aria-pressed="false"></button>
    </div>
  </div>
</div>
```

## CSS

```css
/* 1) Register the palette colours as Houdini custom properties so that CHANGING them
      TRANSITIONS (the mesh crossfades). inherits:true → the child layers pick them up. */
@property --mg-c1{syntax:'<color>';inherits:true;initial-value:#6366f1}
@property --mg-c2{syntax:'<color>';inherits:true;initial-value:#8b5cf6}
@property --mg-c3{syntax:'<color>';inherits:true;initial-value:#22d3ee}
@property --mg-c4{syntax:'<color>';inherits:true;initial-value:#3b82f6}
@property --mg-c5{syntax:'<color>';inherits:true;initial-value:#a855f7}
@property --mg-c6{syntax:'<color>';inherits:true;initial-value:#0ea5e9}

.mg-stage{
  --mg-c1:#6366f1;--mg-c2:#8b5cf6;--mg-c3:#22d3ee;--mg-c4:#3b82f6;--mg-c5:#a855f7;--mg-c6:#0ea5e9;
  position:relative; overflow:hidden; border-radius:28px; min-height:640px;
  background:#0a0a14; isolation:isolate;
  transition:--mg-c1 .9s ease,--mg-c2 .9s ease,--mg-c3 .9s ease,
             --mg-c4 .9s ease,--mg-c5 .9s ease,--mg-c6 .9s ease;   /* the live recolour */
}

/* a palette = just a different set of the same six vars */
.mg-stage.mg-solstice{--mg-c1:#fb7185;--mg-c2:#f472b6;--mg-c3:#fb923c;--mg-c4:#f59e0b;--mg-c5:#e879f9;--mg-c6:#f43f5e;background:#160a10}
.mg-stage.mg-verdant {--mg-c1:#34d399;--mg-c2:#2dd4bf;--mg-c3:#a3e635;--mg-c4:#22d3ee;--mg-c5:#10b981;--mg-c6:#4ade80;background:#04130d}
.mg-stage.mg-orchid  {--mg-c1:#e879f9;--mg-c2:#c026d3;--mg-c3:#8b5cf6;--mg-c4:#f0abfc;--mg-c5:#a21caf;--mg-c6:#7c3aed;background:#120615}

/* 2) THE MESH — oversized so rotation never reveals an edge; blurred for the creamy bleed */
.mg-mesh{position:absolute;inset:0;z-index:0}
.mg-layer{position:absolute;inset:-30%;will-change:transform}
.mg-a{filter:blur(42px); animation:mg-driftA 32s ease-in-out infinite;
  background:
    radial-gradient(42% 46% at 18% 24%,var(--mg-c1) 0%,transparent 60%),
    radial-gradient(40% 44% at 82% 16%,var(--mg-c3) 0%,transparent 58%),
    radial-gradient(48% 52% at 76% 80%,var(--mg-c2) 0%,transparent 60%),
    radial-gradient(46% 50% at 22% 82%,var(--mg-c4) 0%,transparent 60%),
    radial-gradient(40% 42% at 50% 52%,var(--mg-c5) 0%,transparent 55%),
    radial-gradient(54% 58% at 50% -4%,var(--mg-c6) 0%,transparent 62%);
}
.mg-b{filter:blur(48px); mix-blend-mode:screen; opacity:.92; animation:mg-driftB 40s ease-in-out infinite;
  background:
    radial-gradient(50% 54% at 66% 30%,var(--mg-c1) 0%,transparent 62%),
    radial-gradient(46% 50% at 10% 58%,var(--mg-c3) 0%,transparent 60%),
    radial-gradient(48% 52% at 90% 68%,var(--mg-c5) 0%,transparent 60%),
    radial-gradient(42% 46% at 42% 96%,var(--mg-c2) 0%,transparent 58%);
}

/* 3) gentle drift — TRANSFORM only (GPU-cheap), the two layers move in opposition */
@keyframes mg-driftA{0%,100%{transform:translate3d(0,0,0) rotate(0deg) scale(1.08)}
  50%{transform:translate3d(2.5%,-3%,0) rotate(8deg) scale(1.15)}}
@keyframes mg-driftB{0%,100%{transform:translate3d(0,0,0) rotate(0deg) scale(1.1)}
  50%{transform:translate3d(-3%,2.5%,0) rotate(-9deg) scale(1.16)}}

/* the swatch buttons just preview their own palette */
.sw-nebula  {background:radial-gradient(at 30% 26%,#6366f1,transparent 60%),radial-gradient(at 80% 28%,#22d3ee,transparent 58%),#3b82f6}
.sw-solstice{background:radial-gradient(at 30% 26%,#fb7185,transparent 60%),radial-gradient(at 80% 28%,#fb923c,transparent 58%),#f59e0b}

@media (prefers-reduced-motion: reduce){ .mg-a,.mg-b{animation:none} }   /* static mesh */
```

## JavaScript

Optional. Switching palettes is just toggling a class; the parallax is a nicety.

```js
var stage=document.getElementById('mgStage'),
    mesh=document.getElementById('mgMesh'),
    swatches=document.getElementById('mgSwatches'),
    PALS=['nebula','solstice','verdant','orchid'];

swatches.addEventListener('click',function(e){
  var btn=e.target.closest('.mg-sw'); if(!btn) return;
  PALS.forEach(function(p){ stage.classList.remove('mg-'+p); });
  if(btn.dataset.pal!=='nebula') stage.classList.add('mg-'+btn.dataset.pal);  // nebula = default vars
  swatches.querySelectorAll('.mg-sw').forEach(function(b){
    b.setAttribute('aria-pressed', b===btn ? 'true':'false'); });
});

/* gentle pointer-parallax for depth — never on touch / reduced-motion */
if(mesh && !matchMedia('(prefers-reduced-motion: reduce)').matches && !matchMedia('(hover: none)').matches){
  stage.addEventListener('pointermove',function(e){
    var r=stage.getBoundingClientRect();
    var dx=((e.clientX-r.left)/r.width-.5)*-22, dy=((e.clientY-r.top)/r.height-.5)*-16;
    mesh.style.transform='translate3d('+dx.toFixed(1)+'px,'+dy.toFixed(1)+'px,0)';
  });
  stage.addEventListener('pointerleave',function(){ mesh.style.transform='translate3d(0,0,0)'; });
}
```

## How it works

- **The mesh is just stacked `radial-gradient`s.** Each one is a soft blob (`colour 0% → transparent ~60%`); listing six in a single `background` and letting them overlap produces the multi-point colour field. No SVG, no canvas, no `<span>` per blob.
- **`blur()` does the blending.** A heavy blur on each layer melts the individual blobs into one another so you never see a hard radial ring — this is what separates a "mesh" from a few stacked spotlights.
- **Two layers + `mix-blend-mode: screen`** give depth and stop the field looking flat: layer A is the base, layer B adds brighter overlaps that shift as the two drift apart.
- **Drift is `transform`-only.** The layers are sized `inset:-30%` and `scale(1.08+)` so a slow `translate`/`rotate` never exposes a corner, and because only `transform` animates it stays on the compositor (no per-frame repaint).
- **The live recolour is Houdini.** Registering the colour vars with `@property{ syntax:'<color>' }` makes them *animatable*, so a plain `transition` on `.mg-stage` crossfades them; the child gradients read the inherited values and re-render through the tween. Swapping the palette is therefore just `classList` work.

## Customization

- **Add / move blobs:** add or remove `radial-gradient(...)` lines and nudge the `at X% Y%` anchors. 5–7 stops is the sweet spot; more just muddies.
- **Softness:** raise `blur()` (50–70px = dreamier, cloudier) or lower it (20–30px = punchier, more defined pools).
- **Energy:** change the drift `duration` (longer = calmer) and the `rotate`/`translate` amounts in the keyframes.
- **New palette:** copy a `.mg-<name>` rule and supply six colours + a matching dark `background`. Keep one dark base behind the mesh so the colours stay vivid.
- **Blend feel:** swap layer B's `mix-blend-mode` to `overlay`/`soft-light` for a moodier mix, or `lighten` to avoid white-out in bright overlaps.

## Accessibility & performance

- **Respects `prefers-reduced-motion`:** the drift animation and pointer-parallax are both disabled, leaving a fully static (and still beautiful) mesh.
- **Cheap to run:** the ambient motion is compositor-only `transform`. The one repaint-heavy moment is the ~0.9s palette crossfade (animating `background-image` via `@property`), which is fine because it is user-initiated and brief — never put a *continuous* animation on the gradient colours.
- **Contrast:** a mesh is busy. Keep body text inside a calmer zone and/or add a subtle scrim (`radial-gradient`/`linear-gradient` of `rgba(0,0,0,…)`) behind the copy; verify text contrast over the *brightest* blob, not the average.
- Mark the mesh `aria-hidden="true"` — it is decoration. Give each swatch an `aria-label` and toggle `aria-pressed`.

## Gotchas

- **`@property` support:** without it (older Safari/Firefox) the recolour is instant instead of a crossfade — the mesh itself still works, so it degrades gracefully. `@property` must sit at the stylesheet top level (not nested).
- **The transition must live on the element whose class changes** (`.mg-stage`). The child layers only *inherit* the vars, so a `transition` on them wouldn't fire — let the parent tween and the children follow.
- **Don't animate `blur()` or the gradient every frame** — that forces a full repaint and stutters. Animate `transform`; keep the blur static.
- **Oversize the layers.** If a drifting/rotating layer is only `inset:0`, the rotation reveals empty corners. `inset:-30%` + `scale(>1)` keeps full coverage; the parent needs `overflow:hidden`.
- **`transparent` in a gradient = transparent *black*,** which can darken blob edges in sRGB. The heavy blur hides it here; if it bothers you, fade to `color-mix(in oklab, var(--mg-c1), transparent)` instead.
- A parent `filter`/`backdrop-filter` creates a containing block that can clip or flatten `mix-blend-mode` — keep the mesh layers as plain siblings inside the stage.
