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…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/gradient-mesh.jsonPrompt
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-0sheet with no children; content is an ordinaryrelativesibling 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 name —
colorscarries token names ("chart-1"), not color values; the component composesvar(--…)insidecolor-mix(), so the whole palette contract stays inside the theme. - Vars in, animation in a class — dynamic timing travels as custom properties while the
animationshorthand lives in a class, because an inline shorthand would outrank themotion-reduceoverride 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-motionthe blobs freeze rather than disappear: the ambience and the contrast it lends to overlaid text survive, only the movement stops.