Animated gradient backgrounds in Tailwind CSS

Animate a gradient background in Tailwind v4 with one keyframe, blurred blobs, or a live shader.

Updated

To animate a gradient background in Tailwind CSS, make the gradient twice as wide as the element and slide it with a keyframe. In Tailwind v4, add the animation to your CSS:

app/globals.css
@theme {
  --animate-gradient-pan: gradient-pan 12s linear infinite;

  @keyframes gradient-pan {
    from {
      background-position: 0% 50%;
    }
    to {
      background-position: 200% 50%;
    }
  }
}

Then put it on any element with a horizontal gradient that starts and ends on the same colour:

<section
  class="flex h-80 items-end bg-linear-to-r from-indigo-600 via-fuchsia-500 to-indigo-600 bg-[length:200%_100%] p-10 motion-safe:animate-gradient-pan"
>
  <h1 class="text-4xl font-semibold text-white">Ship faster</h1>
</section>

That is the whole technique for a moving linear gradient. This post also covers a softer "mesh" look made of blurred blobs, what each approach costs, and when a live shader component is the better tool.

How the sliding gradient loops

bg-[length:200%_100%] makes the background twice the element's width. Moving background-position from 0% to 200% then shifts it by exactly one background width, and the background repeats. Because the gradient starts and ends on indigo-600, the last frame matches the first, so the loop has no jump. We checked this by rendering frame 0 and the frame at 12 seconds in Chrome: they are pixel-identical.

Three details matter:

  • Keep it horizontal. bg-linear-to-r tiles cleanly side by side. A diagonal gradient such as bg-linear-to-br shows a hard edge where the tiles meet.
  • Use linear timing. With ease-in-out the colours slow down and stop at the end of every loop, which reads as a stutter.
  • Respect reduced motion. motion-safe: only applies the animation when the visitor hasn't asked for reduced motion. They get the still gradient instead.

If you would rather have the colours drift back and forth than travel in one direction, use the common alternative: background-position from 0% 50% to 100% 50% with alternate and ease-in-out. It loops too, but the motion visibly reverses every cycle.

In Tailwind v3, the gradient class is bg-gradient-to-r, and the keyframe and animation go in tailwind.config.js under theme.extend.keyframes and theme.extend.animation.

A mesh-style background with blurred blobs

A sliding gradient is flat: one band of colour moving sideways. For the soft, cloudy look of a mesh gradient, stack a few large blurred circles behind your content and move them with transform:

app/globals.css
@theme {
  --animate-blob: blob 14s ease-in-out infinite;

  @keyframes blob {
    0%,
    100% {
      transform: translate(0, 0) scale(1);
    }
    33% {
      transform: translate(12%, -10%) scale(1.15);
    }
    66% {
      transform: translate(-10%, 8%) scale(0.9);
    }
  }
}
<section
  class="relative isolate flex h-80 items-end overflow-hidden bg-slate-950 p-10"
>
  <div aria-hidden="true" class="pointer-events-none absolute inset-0 -z-10">
    <div
      class="absolute -top-1/4 -left-1/4 aspect-square w-3/5 rounded-full bg-fuchsia-500/60 blur-3xl motion-safe:animate-blob"
    ></div>
    <div
      class="absolute top-1/4 -right-1/4 aspect-square w-1/2 rounded-full bg-sky-400/50 blur-3xl motion-safe:animate-blob motion-safe:[animation-delay:-5s]"
    ></div>
    <div
      class="absolute -bottom-1/3 left-1/4 aspect-square w-1/2 rounded-full bg-violet-500/50 blur-3xl motion-safe:animate-blob motion-safe:[animation-delay:-9s]"
    ></div>
  </div>
  <h1 class="text-4xl font-semibold text-white">Ship faster</h1>
</section>

The keyframe starts and ends on the same transform, so each blob loops cleanly. Negative delays start the blobs at different points of the same path, so they don't move in step. isolate with -z-10 keeps the blobs behind the heading without escaping the section, and overflow-hidden crops them to its edges.

Put the delay behind motion-safe: as well. Tailwind emits motion-safe:animate-blob inside a media query after plain utilities, and the animation shorthand resets animation-delay. A bare [animation-delay:-5s] loses to it and every blob moves in lockstep. With both behind motion-safe:, Chrome reports the delays as 0s, -5s and -9s, as intended.

What each approach costs

Both pure CSS versions are free in JavaScript terms, but they don't cost the same to draw:

  • The sliding gradient repaints. background-position isn't one of the properties browsers animate on the compositor, so the element is repainted every frame. On a small banner that's cheap. On a full-screen hero it's measurable on low-end phones. CSS gradient vs WebGL gradient has a Chrome trace and a transform version that skips the repaints.
  • The blobs mostly don't. transform animations run on the compositor, so the browser moves the already-blurred layers instead of redrawing them. Each blurred layer still takes GPU memory, so keep it to three or four.

Neither one looks like a real mesh gradient up close. The blob version is a few circles crossing each other, and the colours only mix where the blur overlaps.

When to use a live gradient instead

When you want colour that flows and folds rather than slides, a shader does the job in one element. The free gradients.sh components draw the gradient on the GPU (WebGPU, or WebGL2 where WebGPU isn't available), loop seamlessly and take Tailwind classes like any other element. Linear Flow is the closest match to the sliding gradient above: a linear blend whose angle and stops drift slowly.

Keep it moving

Install it with the shadcn CLI:

pnpm dlx shadcn@latest add https://gradients.sh/r/linear-flow.json

The CLI adds components/linear-flow.tsx, the shared components/gradient-surface.tsx and the engine in lib/gradient-engine/. Size and lay it out with Tailwind classes; children render on top:

app/page.tsx
import { LinearFlow } from "@/components/linear-flow";

export default function Page() {
  return (
    <LinearFlow
      className="flex h-80 items-end p-10"
      colors={["#4f46e5", "#d946ef"]}
    >
      <h1 className="text-4xl font-semibold text-white">Ship faster</h1>
    </LinearFlow>
  );
}

colors takes two to six CSS colours: hex, oklch(), or variables like shadcn's var(--primary). Tailwind's own palette variables are a trap here. Tailwind v4 only writes --color-indigo-600 into your CSS when some class uses it, so var(--color-indigo-600) can point at nothing. Pass the value itself, or define your own variable. For the cloudy look of the blob version, install Mesh Gradient instead; it takes the same props.

The engine handles what the CSS versions leave to you: it stops drawing off screen and in hidden tabs, caps the canvas at 1.5× device pixels, and shows one still frame to visitors who prefer reduced motion. It adds about 50 KB of gzipped JavaScript up front, plus about 8 KB that loads later in browsers that draw with WebGPU, all shared by every gradients.sh component on the page.

Which one to use

ApproachLooks likeJavaScriptBest for
Sliding gradientOne band of colour moving sidewaysNoneBanners, buttons, small sections
Blurred blobsSoft circles drifting pastNoneA hero where a few CSS layers are enough
Linear Flow, Mesh GradientColour that flows and foldsAbout 50 KB gzipped, plus 8 KB lazy for WebGPUA hero or section that should feel alive

To pick colours and motion visually instead of by trial and error, tune a gradient in the editor and install exactly what you see.