Stripe gradient animation in React: how it works and how to build it

How the copied Stripe Gradient.js really draws its gradient, what it leaves out, and a maintained React version with a slanted edge.

Updated

To make a Stripe-style animated gradient in React, install the free Mesh Gradient component, pass it four bright colours and cut the bottom edge at an angle with clip-path:

npx shadcn@latest add https://gradients.sh/r/mesh-gradient.json
<MeshGradient
  className="h-[480px] [clip-path:polygon(0_0,100%_0,100%_72%,0_100%)]"
  colors={["#ffba27", "#ef008f", "#7038ff", "#6ec3f4"]}
  background="#ffffff"
  blur={0.25}
  warp={0.7}
/>

A headline on a moving mesh

This isn't Stripe's code and won't match it pixel for pixel. It gets the same feel, saturated colour fields sliding through each other behind a slanted hero, in a component you can install and update. The rest of this post explains how the original works, what the widely copied version of it leaves out, and how to build the hero.

How Stripe's gradient works

Stripe's homepage made this effect famous around 2020. Most tutorials, including Kevin Hufnagl's walkthrough and the Whatamesh project, use a Gradient.js file pulled out of Stripe's minified site and cleaned up. Reading that file shows a different technique from most "mesh gradient" code:

  • It's a real mesh. A small WebGL helper the file calls minigl builds a flat plane of triangles. The plane has about 0.06 segments per pixel across and 0.16 down, so a 1440 by 600 canvas holds 87 × 96 segments, or 16,704 triangles.
  • The vertex shader does the work. Each vertex is pushed up and down by 3D simplex noise (the Ashima Arts implementation), with an amplitude of 320 that fades to zero at the top and bottom edges. That displacement is what makes the colour fields look like folding cloth.
  • Colour is worked out per vertex, not per pixel. The shader starts from the first colour, then layers the other three on top. Each layer is its own noise field, sharpened with smoothstep and raised to the fourth power, so each colour shows up as a distinct blob rather than a haze. The GPU then blends the colours across each triangle. The fragment shader does little more than optionally darken the top edge.
  • Colours come from CSS variables. The script reads --gradient-color-1 to --gradient-color-4 from the canvas element's computed style.
  • Time never loops. The noise moves with a clock that only counts up, so the motion never repeats exactly.

What the copied Gradient.js leaves to you

The file works, and the effect it draws is lovely. It's also a lightly cleaned-up copy of minified production code that Stripe never published as a library, and it shows. In the copy most tutorials link to:

  • Colours must be hex. The script checks that --gradient-color-1 contains a #. If you set oklch(), rgb() or a var() that resolves to anything else, it retries for 200 frames, then starts anyway and parses the value as hex, so an rgb() colour comes out black.
  • Nothing respects reduced motion. The copy keeps an isStatic flag for a still mode, but the line that set it is commented out, and nothing checks prefers-reduced-motion.
  • It doesn't pause off screen. The scroll observer that paused Stripe's animation when the hero left the viewport is commented out too. Only a hidden tab stops it.
  • The width is the window. On resize it sizes the canvas to window.innerWidth, whatever the size of the element you put it in. The height defaults to 600 pixels.
  • It drops frames on purpose. It skips every frame whose timestamp, in whole milliseconds, is even, which works out to about half of them.
  • There's no license. Neither Whatamesh nor the other popular reverse-engineered repo, stripe-gradient, has a license file. Without one, you have no clear right to reuse the code.
  • Nothing shows before it runs. The canvas is blank until the script has loaded and found its colours.

None of these are hard to fix, but you'd own the fixes. The copy weighs 40.8 KB unminified, 9.6 KB gzipped.

The same look with Mesh Gradient

Mesh Gradient gets to a similar result by a different route. It draws one rectangle covering the canvas and works out every pixel's colour in the fragment shader. Each colour is a soft field of light, warped by flow noise. The fields are blended in OKLab with their chroma restored, so a white background between them stays clean instead of washing the colours grey.

Copied Gradient.jsMesh Gradient
GeometryPlane of triangles, noise in verticesOne rectangle, colour per pixel
ColoursFour hex values in CSS variablesTwo to six of any CSS colour, including var()
LoopNever repeatsRepeats exactly every loopSeconds
Before JavaScriptBlank canvasServer-rendered CSS gradient in the same colours
Reduced motionKeeps animatingOne still frame
Off screenKeeps animating (hidden tab pauses)Stops drawing
BackendWebGLWebGPU where available, WebGL2 elsewhere
LicenseNoneMIT, copied into your project

The original does one thing Mesh Gradient doesn't: its colours ride on a displaced surface, so the folds have a slightly 3D quality. If you want the exact Stripe motion, Gradient.js is the closer match. If you want the look in a maintained component, read on.

1. Install

You need a React project with a components.json. Run npx shadcn@latest init first if you don't have one. Next.js, Vite, React Router and TanStack Start all work.

pnpm dlx shadcn@latest add https://gradients.sh/r/mesh-gradient.json

The CLI copies components/mesh-gradient.tsx, the shared components/gradient-surface.tsx and the engine in lib/gradient-engine/ into your project.

2. Build the hero

Mesh Gradient is a div that fills its box with the gradient and renders children on top. Give it a height, the four colours and a slanted bottom edge:

components/hero.tsx
import { MeshGradient } from "@/components/mesh-gradient";

export function Hero() {
  return (
    <MeshGradient
      className="flex min-h-[560px] flex-col justify-center px-8 [clip-path:polygon(0_0,100%_0,100%_72%,0_100%)]"
      colors={["#ffba27", "#ef008f", "#7038ff", "#6ec3f4"]}
      background="#ffffff"
      blur={0.25}
      warp={0.7}
    >
      <h1 className="max-w-xl text-5xl font-semibold tracking-tight text-white [text-shadow:0_1px_16px_rgb(0_0_0/0.35)]">
        Your product, on a moving gradient
      </h1>
    </MeshGradient>
  );
}

What each change from the defaults does:

  • colors takes two to six colours. The first sits in the lit corner and the last in the shadow, so put the warm colours first. These four are the orange, magenta, violet and sky blue most Stripe clones use.
  • background is the colour between the fields. White keeps the gaps bright, like Stripe's. The default is a deep navy.
  • blur={0.25} makes the fields crisper than the default 0.45.
  • warp={0.7} bends the fields more than the default 0.4, so they fold into each other.
  • The soft text-shadow keeps white text readable when a pale colour or the white background drifts behind it. Check contrast with your own colours.

The component file starts with "use client", so a Next.js Server Component can import it. The server renders a CSS approximation of the colours at full size, and the first GPU frame replaces it in place, so nothing shifts while the page loads.

3. The slanted edge: clip-path or skewY

Stripe's hero tilts its section with a skew. Hufnagl's version sets --section-skew-Y: -12deg, plus a value of 0.212 that it uses to size the gradient so it still covers the corners. The example above uses clip-path instead. Both give a diagonal bottom edge, and they behave differently:

  • clip-path cuts the box along a polygon. The gradient isn't distorted and the box keeps its height. Anything outside the polygon is hidden, including shadows and anything that overflows, and the slant stays as a share of the height, so the angle changes as the hero gets wider. polygon(0 0, 100% 0, 100% 72%, 0 100%) drops the right side to 72% of the height.
  • skewY shears the whole box. A skew of θ moves the far edge by the width times tan θ, so at 12° the right edge sits about 0.21 of the width higher than the left, which matches Hufnagl's 0.212. That's 306 pixels on a 1440-pixel screen. The angle stays fixed at any width, but the box loses that much height on one side, and its content is sheared too, unless you skew a background layer and leave the content alone.

For a skewed version, put the gradient on its own layer:

<section className="relative isolate overflow-hidden pt-40 pb-32">
  <MeshGradient
    className="absolute inset-0 -z-10 origin-bottom-left -skew-y-12"
    colors={["#ffba27", "#ef008f", "#7038ff", "#6ec3f4"]}
    background="#ffffff"
    blur={0.25}
    warp={0.7}
  />
  {/* Content stays unskewed. */}
</section>

origin-bottom-left pins the left end of the bottom edge and -skew-y-12 lifts everything to its right, so the top stays covered and the bottom becomes the diagonal. In Chrome, a 640-pixel-tall layer at 1440 wide comes out covering the full height on the left and the top 335 pixels on the right. Keep the section taller than about 0.21 of its width, or the diagonal runs out before the right edge. The gradient is sheared along with its layer, which reads as part of the motion.

4. Tune the motion

Stripe's gradient moves slowly. The mesh defaults are close. For a calmer hero, try speed={0.4} and loopSeconds={20}. grain={0} gives the clean finish Stripe has, and a different seed rearranges the fields. The Mesh Gradient props table lists everything.

To find values by eye, open the editor, pick the mesh look, set the colours and copy the install command for what you see. Tuning is open to everyone; copying the command needs a free account. The mesh gradient generator page has mesh palettes that open straight in the editor.

5. Use your brand colours

This is where the component and the copied script differ most. Any CSS colour works, including theme variables, so the gradient can follow your design tokens:

<MeshGradient
  className="h-[480px]"
  colors={[
    "var(--chart-1)",
    "var(--primary)",
    "var(--chart-4)",
    "var(--chart-2)",
  ]}
  background="var(--background)"
/>

When the class on <html> changes, the component re-reads the variables, so a dark mode toggle updates the gradient too. Gradients that follow light and dark mode covers the CSS side.