How to make a mesh gradient in CSS and React

Three ways to build a mesh gradient: stacked radial gradients in CSS, a static image, or a live WebGL component.

Updated

To make a mesh gradient in CSS, stack several radial-gradient layers, each one a colour placed at a different point that fades to transparent, over a solid background colour. It needs no JavaScript and no image:

.mesh {
  background-color: #0a0d24;
  background-image:
    radial-gradient(85% 95% at 16% 20%, #ecdcf2 0%, transparent 68%),
    radial-gradient(85% 95% at 84% 26%, #8796ee 0%, transparent 68%),
    radial-gradient(85% 95% at 72% 86%, #2e45b7 0%, transparent 68%),
    radial-gradient(85% 95% at 22% 80%, #0e1645 0%, transparent 68%);
}

Pure CSS

That covers a still background. For a mesh that moves, you can animate the CSS a little, export a still or video, or draw it live in WebGL with a React component. This post covers all three and when each is the right call.

What a mesh gradient is

A linear or radial gradient blends colours along one line or out from one point. A mesh gradient places colours at several points across a surface and blends between all of them, so the colour changes in two directions at once. Design tools such as Illustrator and Figma build them from a grid of colour points you can drag. On the web, you fake the grid with soft blobs of colour that overlap.

1. Pure CSS

Each radial-gradient in the snippet above is one colour point:

  • at 16% 20% places the point. Spread the points out, and keep one or two near the edges so the corners aren't empty.
  • 85% 95% is the size of the ellipse, as a share of the box. Bigger ellipses overlap more and blend more softly.
  • #ecdcf2 0%, transparent 68% makes the colour solid at the centre and fully clear at 68% of the ellipse. Lower that number for tighter, more distinct blobs.
  • background-color shows wherever no blob reaches, and tints the gaps between them.

The first layer in the list is painted on top. Put the colour you want to read most clearly first.

CSS has one limit here: the layers are stacked with alpha compositing, so where two blobs overlap, the top one partly covers the bottom one rather than mixing with it evenly. With colours that sit close together, like the blues above, you won't notice. With opposite colours, such as orange and blue, the overlap can look muddy. Add a third blob of an in-between colour where they meet, or keep opposites further apart.

CSS Hero's Mesher generates this kind of CSS if you'd rather drag points than type percentages.

Animating it

You can't transition a gradient directly, but you can animate the custom properties a gradient reads, once they're registered with @property so the browser knows they're percentages:

@property --a-x {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 16%;
}
@property --a-y {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 20%;
}
@property --b-x {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 84%;
}
@property --b-y {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 26%;
}

.mesh-animated {
  background-color: #0a0d24;
  background-image:
    radial-gradient(
      85% 95% at var(--a-x) var(--a-y),
      #ecdcf2 0%,
      transparent 68%
    ),
    radial-gradient(
      85% 95% at var(--b-x) var(--b-y),
      #8796ee 0%,
      transparent 68%
    ),
    radial-gradient(85% 95% at 72% 86%, #2e45b7 0%, transparent 68%),
    radial-gradient(85% 95% at 22% 80%, #0e1645 0%, transparent 68%);
  animation: mesh-drift 14s ease-in-out infinite alternate;
}

@keyframes mesh-drift {
  to {
    --a-x: 40%;
    --a-y: 45%;
    --b-x: 62%;
    --b-y: 8%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .mesh-animated {
    animation: none;
  }
}

alternate plays the animation forwards, then backwards, so it never jumps back to the start. The blobs slide around, but their shapes stay round, so it reads as drifting lights rather than colours folding into each other.

Animating a gradient repaints the whole box on every frame, on the main thread. That's fine for a card. On a full-screen hero, check it in the Performance panel on a slow phone before you ship it.

2. A static image

If the gradient doesn't need to move or follow your theme, an image is the simplest thing to ship. Make the mesh in a generator, export a PNG or WebP, and use it as a background-image or an <img>.

The gradients.sh mesh gradient generator exports PNG up to 1920px wide on a free account. meshgradientgenerator.com exports SVG, CSS, PNG, WebP, JPG and WebM with no sign-up. An image costs one request and its file size, and it can't change with dark mode or a rebrand without a new export.

For a mesh that moves without JavaScript, export it as a seamless MP4 loop. Check the file size first: background video vs animated gradient has measured numbers, and grain makes video much heavier.

3. A live mesh gradient in React

For colours that fold into each other, keep moving and follow your theme, draw the mesh on the GPU. The free Mesh Gradient component does that:

Ship faster

Install it into any React project with a components.json (run npx shadcn@latest init first if you don't have one):

pnpm dlx shadcn@latest add https://gradients.sh/r/mesh-gradient.json
components/hero.tsx
import { MeshGradient } from "@/components/mesh-gradient";

export function Hero() {
  return (
    <MeshGradient
      className="flex h-[480px] items-end p-8"
      colors={["#ecdcf2", "#8796ee", "#2e45b7", "#0e1645"]}
      background="#0a0d24"
    >
      <h1 className="text-5xl font-semibold tracking-tight text-white">
        Ship faster
      </h1>
    </MeshGradient>
  );
}

Those are the component's default colours, the same four as the CSS version, so you can compare the two directly. The differences come from how the shader draws them:

  • Colours blend in OKLab, not by stacking. Every pixel is a weighted mix of all the colour points, computed in the OKLab colour space, where equal steps look equally different to the eye. The shader also restores the colourfulness that averaging loses, so where opposite colours meet they stay vivid instead of going muddy like the CSS overlap.
  • The fields warp. Noise bends the space before the colours are placed, so the blobs stretch and fold into each other. warp sets how much (default 0.4).
  • It loops exactly. The colour points travel on a path that repeats every loopSeconds (default 14), with no reverse and no seam. How seamless gradient loops work explains the maths.
  • It reads your CSS variables. colors={["var(--primary)", …]} follows your design tokens and dark mode. See gradients that follow light and dark mode.

The server renders the pure CSS version from section 1, built from the same colours, so the page never shows an empty box. The first GPU frame replaces it in place.

Change colors (two to six), blur, warp, scale and seed to reshape it. The Mesh Gradient props table lists every prop, and the editor lets you tune by eye and copy the install command for exactly what you see. For a white, slanted, Stripe-like version, see Stripe-style animated mesh gradient.

What it costs

  • About 50 KB of gzipped JavaScript for the first component, engine included, plus about 8 KB loaded later on browsers that use WebGPU.
  • WebGPU where available, WebGL2 elsewhere. A browser with neither keeps the CSS fallback.
  • Drawing stops when the gradient is off screen or the tab is hidden, and visitors who prefer reduced motion get one still frame.

Which one to use

You needUse
A still background, zero JavaScriptPure CSS
A slow drift on a small elementCSS with @property
An exact look from a design tool, no themingPNG or WebP
Motion with no JavaScript, in a deck or postMP4 loop
Colours that fold, loop and follow your themeMesh Gradient (WebGL)