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
import { MeshGradient } from "@/components/mesh-gradient";
export function StripeStyleMeshDemo() {
return (
<MeshGradient
className="flex h-[360px] items-start p-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}
>
<h2 className="text-3xl font-semibold tracking-tight text-white [text-shadow:0_1px_16px_rgb(0_0_0/0.35)]">
A headline on a moving mesh
</h2>
</MeshGradient>
);
}
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
miniglbuilds 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
smoothstepand 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-1to--gradient-color-4from 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-1contains a#. If you setoklch(),rgb()or avar()that resolves to anything else, it retries for 200 frames, then starts anyway and parses the value as hex, so anrgb()colour comes out black. - Nothing respects reduced motion. The copy keeps an
isStaticflag for a still mode, but the line that set it is commented out, and nothing checksprefers-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.js | Mesh Gradient | |
|---|---|---|
| Geometry | Plane of triangles, noise in vertices | One rectangle, colour per pixel |
| Colours | Four hex values in CSS variables | Two to six of any CSS colour, including var() |
| Loop | Never repeats | Repeats exactly every loopSeconds |
| Before JavaScript | Blank canvas | Server-rendered CSS gradient in the same colours |
| Reduced motion | Keeps animating | One still frame |
| Off screen | Keeps animating (hidden tab pauses) | Stops drawing |
| Backend | WebGL | WebGPU where available, WebGL2 elsewhere |
| License | None | MIT, 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.jsonThe 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:
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:
colorstakes 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.backgroundis 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 default0.45.warp={0.7}bends the fields more than the default0.4, so they fold into each other.- The soft
text-shadowkeeps 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-pathcuts 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.skewYshears 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.
Related
- How to make a mesh gradient in CSS and React builds a similar effect with stacked radial gradients and no JavaScript, and says when that's enough.
- React libraries for animated gradient backgrounds, compared weighs Mesh Gradient against Paper Shaders' MeshGradient and other options, with measured bundle sizes.
- CSS gradient vs WebGL gradient measures what each costs the browser.
- Export an animated gradient as a seamless MP4 loop turns the gradient into a video for slides and social posts.
- Gradient text in Tailwind fills a headline with the same moving mesh.