Backgrounds

Gradient Mesh

A full-bleed mesh-gradient wash — oversized blurred token blobs drifting past each other as an absolute background layer.

Preview in your theme

Loading preview…

import * as React from "react"
import { cn } from "@/lib/utils"

/**
 * Keyframes ship inside the component via React 19 hoisted <style> —
 * no tailwind config edits, and duplicates dedupe by href.
 * Every pose stays within ±10% of the blob's own size, so a drifting blob
 * can never slide off its patch of the field and uncover the surface.
 */
const KEYFRAMES = `@keyframes zy-mesh-a{0%,100%{transform:translate3d(0,0,0) scale(1)}33%{transform:translate3d(8%,7%,0) scale(1.12)}66%{transform:translate3d(-7%,10%,0) scale(0.94)}}
@keyframes zy-mesh-b{0%,100%{transform:translate3d(0,0,0) scale(1.08)}33%{transform:translate3d(-9%,6%,0) scale(0.95)}66%{transform:translate3d(8%,-7%,0) scale(1.14)}}
@keyframes zy-mesh-c{0%,100%{transform:translate3d(0,0,0) scale(0.96)}40%{transform:translate3d(7%,-9%,0) scale(1.1)}70%{transform:translate3d(-8%,-5%,0) scale(1.03)}}`

/** color-mix alpha tier — the single knob for how loud the wash is. */

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/gradient-mesh.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript + Tailwind "GradientMesh" component — a mesh-gradient
background layer. Its only dependency is a cn() class merger (clsx +
tailwind-merge). No hooks, no events, no browser APIs, so it must stay a server
component — do NOT add "use client".

Contract
- export function GradientMesh(props): props extend
  Omit<React.ComponentProps<"div">, "children"> (rest props + ref spread onto
  the root div) plus:
  - speed?: number (default 20) — seconds for one drift cycle of the base blob;
    the others derive their durations from it.
  - blur?: number (default 80) — px blur radius on every blob.
  - intensity?: "subtle" | "medium" | "bold" (default "medium") — selects a
    color-mix alpha tier for the blobs: 22% / 38% / 55%.
  - blobCount?: number (default 4) — clamped to 3..5.
  - colors?: string[] (default ["chart-1", "chart-3", "chart-5"]) — theme token
    names WITHOUT the leading "--", cycled across the blobs.
- It renders no children: it is a layer, not a wrapper. The consumer puts it
  inside a `relative` ancestor and writes content next to it in a `relative`
  sibling, which stacks above it without any z-index bookkeeping.

Behavior
- Root div: pointer-events-none absolute inset-0 overflow-hidden, aria-hidden.
  It publishes two CSS custom properties via inline style — --zy-mesh-speed =
  `${speed}s` and --zy-mesh-alpha = the tier percentage — and spreads the
  consumer `style` after them so both stay overridable per instance.
- A fixed table of up to five blob poses (left / top / width / height / which
  keyframe / duration factor / delay factor) is sliced by blobCount. Order the
  table so the first three already cover the whole field.
- Every pose overhangs at least one edge and is TALLER than the container
  (height 105–125%), and each drift keyframe stays within ±10% of the blob's
  own size — together that is what guarantees a drifting blob never slides
  away and uncovers bare background in a short hero.
- Three drift keyframes (a / b / c) cycle transform between three poses
  (translate3d in own-size percentages + scale 0.94–1.14). Per blob the
  duration is calc(var(--zy-mesh-speed) * factor) with factors 1 / 1.35 / 0.85
  / 1.15 / 1.6 and a NEGATIVE delay of calc(var(--zy-mesh-speed) * -0.x), so
  the loops start mid-cycle and never visibly sync.
- Pass the per-blob animation name / duration / delay as CSS custom properties
  in inline style and apply them through one static arbitrary class
  [animation:var(--zy-blob-anim)_var(--zy-blob-dur)_ease-in-out_var(--zy-blob-delay)_infinite].
  Do NOT write the `animation` shorthand inline: inline styles outrank classes
  and would defeat the motion-reduce override below.
- The @keyframes ship inside the component via a React 19 hoisted
  <style href="zyeon-gradient-mesh" precedence="medium"> tag — no Tailwind
  config edits, and multiple instances dedupe to one style tag by href.
- prefers-reduced-motion: every blob gets motion-reduce:[animation:none] but
  stays rendered — the composition (and the contrast it provides for text on
  top) is preserved, only the drifting stops. Never hide the layer: a
  disappearing background leaves a blank panel.

Rendering & styling
- Semantic tokens only. Each blob is
  radial-gradient(closest-side, color-mix(in oklab, var(--<token>)
  var(--zy-mesh-alpha), transparent), transparent) with the token taken from
  the `colors` array — no hex / rgb() / oklch() anywhere, so the mesh re-skins
  itself with the host theme and dark mode.
- rounded-full + a large blur is what fuses separate blobs into one mesh; the
  alpha ceiling (55% at "bold") keeps foreground text readable in both schemes.
- Merge consumer className via cn() on the root — the call site can retarget
  the layer (inset-y-0 right-0 w-1/2), change its stacking (z-0) or round it.

Customization levers
- Palette: the `colors` prop takes token names — ["primary", "chart-2",
  "chart-4"] for brand-tinted, ["chart-1"] alone for a monochrome wash. Length
  is free; blobs cycle through it.
- Loudness: the ALPHA map is the single source of intensity; retune 22/38/55
  or add a tier (e.g. faint: "12%") without touching anything else.
- Softness vs. definition: blur — 100+ px dissolves the blobs into one field,
  40–56 px keeps them readable as distinct shapes.
- Pace: speed is seconds per cycle; 30–45 s reads as ambient, under 12 s starts
  competing with content.
- Density / layout: add a sixth pose to the table (one object) — keep the
  overhang + >100% height rules and the coverage guarantee holds.
- Scope: it is just an absolute layer, so it works inside a card header, a
  sidebar or a banner, not only page heroes; the parent's overflow-hidden and
  rounding clip it to any shape.

Concepts

  • Layer, not wrapper — the component is an absolute inset-0 sheet with no children; content is an ordinary relative sibling that paints above it, so a background can be added to an existing section without re-nesting the markup.
  • Coverage guarantee — blobs are oversized, overhang the edges and drift only within ±10% of their own size, which is the rule that stops a slow-moving mesh from exposing bare background in a short container.
  • Token palette by namecolors carries token names ("chart-1"), not color values; the component composes var(--…) inside color-mix(), so the whole palette contract stays inside the theme.
  • Vars in, animation in a class — dynamic timing travels as custom properties while the animation shorthand lives in a class, because an inline shorthand would outrank the motion-reduce override and silently break the reduced-motion promise.
  • Phase-staggered drift — five loops share one base duration at 1x / 1.35x / 0.85x / 1.15x / 1.6x with negative delays, so the composition never visibly repeats even though each loop is short.
  • Reduced-motion honesty — under prefers-reduced-motion the blobs freeze rather than disappear: the ambience and the contrast it lends to overlaid text survive, only the movement stops.

On This Page