GODRICH

Mesh Gradient CSS, Grain Left In on Purpose

Mesh gradient CSS blends blurred color blobs across a dark canvas like an aurora, then adds film grain so it doesn't read as another AI-smooth template —

The one-page build below stitches all nine sections together. Scroll through it first and watch where the aurora gets dense and where it thins out.

Open the full page

Each section also appears on its own in the grid. The order isn't by popularity — it follows how a visitor actually reads a page: notice it, skim the features, check the price, trust the review, then click.

Auto-plays · click a tile to jump to its section · all nine in one zip

01Hero

Three blurred, colored blobs drift behind the headline at slightly different rhythms, overlapping as they move. The card itself stays a fixed dark ink; the aurora is only ever light passing over it.

blur(60px)translate+rotate그레인 오버레이
.hero-a__orb { position: absolute; width: 46%; aspect-ratio: 1;
  border-radius: 50%; filter: blur(28px); opacity: .85; }
.hero-a__orb--1 {
  background: radial-gradient($color 0%, $color 35%, transparent 68%);
  top: -10%; left: 4%;
  animation: driftA $duration steps(4, jump-none) infinite;
}
@keyframes driftA {
  0%   { transform: translate(0,0) rotate(0deg); }
  50%  { transform: translate(22px, 16px) rotate(20deg); }
  100% { transform: translate(0,0) rotate(0deg); }
}

02Navigation

The nav background is nearly transparent — just a thin sheet of ink-tinted glass underneath. The pill marking the active item is sized by splitting the three links into equal thirds by percentage, not by a guessed pixel width.

backdrop-filter: blurtranslateXborder 반투명
.nav-a {
  background: rgba($stage-ink, .38);
  backdrop-filter: blur(10px);
  border: 1px solid rgba($stage-paper, .18);
}
.nav-a__pill {
  position: absolute; top: $sp-1; bottom: $sp-1; left: $sp-1;
  width: calc((100% - #{$sp-1 * 2}) / 3);
  border-radius: $r-pill; background: $color;
  animation: pillSlide $duration $ease-spring infinite;
}

03Card grid

Only a ring at the card's border is cut out to show a three-color aurora stripe, and that stripe keeps flowing. The card body underneath stays a fixed dark ink so the eye stays on the edge.

repeating-linear-gradient@keyframes sweepmask 이중 레이어
.card-a::before {
  content: ""; position: absolute; inset: 0;
  border-radius: $r-card; padding: $sp-1;
  background: repeating-linear-gradient(120deg,
    $color 0 26px, $stage-yellow 26px 52px,
    $stage-orange 52px 78px, transparent 78px 104px);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  animation: sweep $duration linear infinite;
}
@keyframes sweep { to { background-position: 208px 0; } }

04Feature list

Each feature gets one round badge, and only the gradient inside that badge drifts side to side. Moving the whole row would be distracting; one small drifting dot per line is enough to hold the eye.

background-size 200%background-positionradius pill
.feat-a__icon {
  width: $sp-8; height: $sp-8; border-radius: $r-pill;
  background: linear-gradient(120deg, $color, $stage-yellow, $stage-orange, $color);
  background-size: 200% 100%;
  animation: iconFlow $duration ease-out infinite;
}
@keyframes iconFlow {
  0%   { background-position: 0% 50%; }
  50%  { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

05Pricing

Only the recommended plan gets an aurora gradient that brightens and dims like breathing, drifting slightly as it goes. The other plan stays a faint glass tile so the contrast does the selling.

radial-gradient 위치 이동opacity 호흡badge 고정
.pricing-a__plan--pro::before {
  content: ""; position: absolute; inset: -20%;
  background:
    radial-gradient(circle at 30% 20%, rgba($color, .9), transparent 60%),
    radial-gradient(circle at 75% 80%, rgba($stage-orange, .85), transparent 60%);
  animation: breathe $duration steps(4, jump-none) infinite;
}
@keyframes breathe {
  0%, 100% { opacity: .3; transform: translate(0, 0); }
  50%      { opacity: 1;  transform: translate(-8%, 8%); }
}

06Testimonial

Two blurred aurora blobs drift slowly behind a frosted quote card. The glass in front only gets a light blur so the text stays crisp while the color behind it stays soft.

backdrop-filter: blurtranslate 루프border 반투명
.test-a__orb {
  position: absolute; width: 50%; aspect-ratio: 1;
  border-radius: 50%; filter: blur(20px);
}
.test-a__orb--1 {
  background: radial-gradient($subject-blue 0%, $subject-blue 45%, transparent 72%);
  top: -8%; left: -8%;
  animation: floatA $duration steps(4, jump-none) infinite;
}
.test-a__card {
  background: rgba($stage-ink, .22);
  backdrop-filter: blur(4px);
  border: 1px solid rgba($stage-paper, .18);
}

07CTA band

A single full-width band carries a five-color aurora that keeps flowing, with a static grain layer on top so the screen reads like a printed poster. The whole band is the moving subject here, which puts its changed-pixel area among the largest in the set — third out of nine.

background-position 애니메이션mix-blend-mode: overlayradius card
.cta-a {
  background: linear-gradient(120deg,
    $stage-ink, $color, $stage-orange, $stage-yellow, $stage-ink);
  background-size: 300% 100%;
  animation: flow $duration linear infinite;
}
@keyframes flow {
  0%   { background-position: 0% 50%; }
  100% { background-position: 100% 50%; }
}

08Footer

The film grain jumps to a new spot every frame instead of drifting smoothly, which is what actually reads as grain. Swapping the timing function for steps() is the whole trick — it's the only section where the grain itself is the animation.

steps() 그레인background-position 랜덤칸 배경 5%
.footer-a::before {
  content: ""; position: absolute; inset: 0;
  opacity: .45; mix-blend-mode: overlay;
  background-image:
    radial-gradient(rgba($color, .7) .6px, transparent .6px),
    radial-gradient(rgba($stage-ink, .7) .6px, transparent .6px);
  background-size: 3px 3px, 5px 5px;
  animation: grainJump $duration steps(6, jump-none) infinite;
}
@keyframes grainJump {
  0%   { background-position: 0 0, 0 0; }
  40%  { background-position: 5px -3px, -4px 6px; }
  100% { background-position: 0 0, 0 0; }
}

09Buttons & inputs

The gradient inside the button only flows on hover, while the input skips a focus ring in favor of a constant, soft double box-shadow glow. This is the only section triggered by hover, so the is-demo class forces it to play for capture.

background-position 호버box-shadow 이중 글로우radius control
.bi-a__btn {
  background: linear-gradient(120deg, $color, $stage-orange, $subject-blue, $color);
  background-size: 260% 100%;
}
.bi-a__btn:hover, .bi-a__btn:focus-visible {
  animation: shift $duration ease-out infinite;
}
.bi-a__input {
  box-shadow: 0 0 0 2px rgba($subject-blue, .55), 0 0 16px rgba($stage-orange, .45);
}

Custom tailwind.config

Every color and radius that the nine sections reuse lives in this one config, so swapping the three aurora hues here carries across all of them — with two exceptions worth knowing before you touch it. Grain strength isn't in this file at all; it's a hardcoded opacity inside the one-page build's own <style> block. And the bg-aurora gradient utility repeats the same three hex values as literals rather than pulling from colors above it, so changing a color here won't retint that one utility on its own — you'd edit both places.

theme: {
  extend: {
    colors: {
      ink: "#17141a", cream: "#fff7e6", paper: "#ffffff",
      blue: "#2f6df6", yellow: "#ffd23f", orange: "#ff4d1f",
    },
    borderRadius: { tile: "16px", control: "12px", pill: "999px" },
    backgroundImage: {
      aurora: "linear-gradient(120deg, #17141a, #2f6df6, #ff4d1f, #ffd23f, #17141a)",
    },
  },
},

Vibe-coding prompt

Drop the config above into your project, then hand Claude Code or Cursor the fragment below — it's enough on its own. The full prompt lives in prompt.md inside the zip.

Background is ink only. Only where you want emphasis, overlap 2-3 blurred
(40-60px) blue/yellow/orange blobs so they bleed like an aurora. Never
paint the whole background with a gradient.
Layer faint grain (a radial-gradient dot pattern, opacity around .3) over
every dark surface. Build it with CSS only, never an image file.

Applying this instruction to page/index.html and capturing the full page at a 1280px viewport produced the screenshot below.

result

Where it breaks (the trap)

Moving the aurora blobs with a soft ease curve alone can make an animation invisible in practice — behind a heavy blur, the change between adjacent frames drops too low to register. The first version of section 05's recommended plan only swung opacity from .55 to 1, and the render measurement came back with an intensity of zero; only after widening the range to .3–1 and switching the timing to steps(4, jump-none) did it clear six moved frames. Reaching for semantic tags like <ul> or <figure>/<blockquote> without a second thought is another trap — their default browser margins survive unless you reset them, and at a 320px width that pushes the page taller than the viewport. Sections 04 and 06 hit exactly this, and the scrollbar only disappeared once margin: 0 was added explicitly. The zip holding all nine sections and the full one-page source needs the password u7eepwrp to open, and the button below starts the download.

The last trap is contrast. Desaturating the aurora blobs or switching to a light background makes text sitting on top of the grain lose contrast fast, so any text layer is best kept on the fixed dark ink background rather than directly on the gradient.

Accessibility

Every animated section, including the gradient flow on the section 09 button, is wrapped in the media query defined by the MDN prefers-reduced-motion documentation, so a visitor with reduced motion turned on sees a static version of each one instead. Section 03's mask-composite border follows the MDN mask-composite documentation and keeps the older Safari fallback -webkit-mask-composite: xor alongside the standard value. Every quote and button label is set as white text on ink or ink text on cream, never directly on the moving gradient, so contrast holds up while the colors are shifting.

If this particular look isn't the one you need, the design trends category collects the rest, and the about page explains what this site covers overall.

FAQ

Can mesh gradient CSS be built with CSS alone?

Yes — all nine sections in this set ship with zero lines of JavaScript. Even the hover response on the section 09 button runs entirely on the :hover and :focus-visible pseudo-classes; the only script involved is a short one that forces the animation to play while capturing the demo.

Does the grain texture require an image file?

No. The grain here is two tiny radial-gradient dots layered and tiled at a 3–5px background size, so there isn't a single image request anywhere. Nudging the dot position every frame, as section 08's footer does, is enough to fake the flicker of real film grain with CSS alone.

Does this only work on a dark background?

The aurora blobs themselves still work on a light background, but all nine sections here default to a dark ink base. On a light background, lower the blob opacity and swap the grain's blend mode from overlay to multiply to keep a similar amount of texture.

Enter the archive password

The password is inside this article. You will find it as you read.