An animated mesh gradient hero background for React. A 3D ground plane is displaced by simplex noise and painted with a five-colour blend that slowly drifts, rendered with three.js. Drop it behind a headline and you get a soft, living landscape instead of a flat gradient.
- Five-colour palette blended per-vertex with layered noise, so the colours melt into each other organically instead of banding.
- Fully configurable: hill height, animation speed, wave frequency, and camera rotation are all props, and changing them never rebuilds the WebGL scene (uniforms are mutated in place).
- Camera orbit: the
cameraAngleandcameraPitchprops rotate and tilt the whole landscape while the plane is automatically re-fitted to the camera's ground footprint, so the page never peeks through at any angle or aspect ratio. - Drag to orbit: set
interactiveand users can drag on the hero to orbit (horizontal) and tilt (vertical) the camera, with anonCameraChangecallback to keep external controls in sync. - Wireframe mode: flip the
wireframeprop to render the displaced plane as a mesh of lines. - Performance minded: single WebGL context, capped pixel ratio,
low-powerGPU hint, and the render loop pauses when the component is off-screen or the tab is hidden. - Respects
prefers-reduced-motion: renders a single static frame for users who opt out of animation. - Resize-aware: a
ResizeObserverkeeps the canvas and camera in sync with the container. - Clean teardown: geometry, material, and the GL context are disposed on unmount.
npm install react-hero-mesh threereact (18+) and three (0.152+) are peer dependencies (0.152 introduced the colour-space API the shader colours rely on).
Prefer to own the code? The whole component is a single file: copy src/index.tsx into your project and install three.
The component fills its nearest positioned ancestor (it renders position: absolute; inset: 0), so give the hero container position: relative and put your content above it:
import { HeroMesh } from "react-hero-mesh";
export function Hero() {
return (
<section style={{ position: "relative", overflow: "hidden", borderRadius: 24 }}>
<HeroMesh
colors={["#E3FBEF", "#C9F0D6", "#EAF7C7", "#FCE7D6", "#EFE7FF"]}
amount={0.2}
speed={0.02}
frequency={3}
cameraAngle={0}
/>
<div style={{ position: "relative", zIndex: 1, padding: "6rem 2rem", textAlign: "center" }}>
<h1>How can we help?</h1>
</div>
</section>
);
}All props are optional; with no props you get the default "Meadow" palette.
The component is client-only (it needs a WebGL context) and ships with a "use client" directive. Because three.js is a heavy chunk, defer it:
import dynamic from "next/dynamic";
const HeroMesh = dynamic(() => import("react-hero-mesh"), { ssr: false });| Prop | Type | Default | Range | Description |
|---|---|---|---|---|
colors |
[string, string, string, string, string] |
["#E3FBEF", "#C9F0D6", "#EAF7C7", "#FCE7D6", "#EFE7FF"] |
hex | Five hex colours. The fifth is the base coat; the first four are layered over it with noise. |
amount |
number |
0.2 |
0 to 1 |
Hill height. 0 is a flat sheet, 1 is pronounced rolling dunes. |
speed |
number |
0.02 |
0 to 2 |
How fast the terrain and colours evolve. Even 0 keeps a very slow drift. |
frequency |
number |
3 |
0.2 to 8 |
Visible wave count. Low values give broad swells, high values give choppy ripples. |
cameraAngle |
number |
0 |
0 to 360 |
Orbit azimuth in degrees. Rotates the whole landscape in view. |
cameraPitch |
number |
42 |
~25 to 85 |
Downward camera tilt in degrees. Clamped at runtime to the shallowest tilt that still lets the plane cover the container at the current aspect ratio (wider containers need steeper minimum tilts). |
wireframe |
boolean |
false |
Render the displaced plane as a wireframe instead of filled colour. | |
interactive |
boolean |
false |
Let users drag on the container to orbit (horizontal) and tilt (vertical) the camera. | |
onCameraChange |
(camera: { angle: number; pitch: number }) => void |
Fires as the user drags, with the resulting camera angles. Use it to keep sliders or other controls in sync. | ||
className |
string |
Extra class for the wrapper div. | ||
style |
CSSProperties |
Extra styles for the wrapper div, merged over the absolute-fill defaults. |
The stated ranges are the values the visuals were tuned for, not hard limits.
HERO_MESH_PRESETS exports five ready-made palettes (meadow, sunrise, lagoon, ember, forest), and HERO_MESH_DEFAULTS holds the default values for every prop:
import { HeroMesh, HERO_MESH_PRESETS } from "react-hero-mesh";
const forest = HERO_MESH_PRESETS.find((p) => p.id === "forest")!;
<HeroMesh colors={forest.colors} />;An interactive playground lives in demo/: colour pickers with hex inputs, sliders for every prop, presets, randomise, wireframe, and drag-to-orbit on the hero itself. "Copy link" serialises the whole configuration into the URL so a look can be shared (e.g. /?preset=forest&angle=120&pitch=60).
cd demo
npm install
npm run devA unit plane (240x240 segments) is laid flat and displaced upward in the vertex shader by 3D simplex noise, with time as the third dimension so the terrain rolls. The same shader blends the five colours per-vertex: the base colour is progressively mixed toward each of the other four using smoothstep-shaped noise at slightly different flow speeds and seeds, which is what produces the soft, marbled patches. The fragment shader is flat (no lighting); the entire look comes from the colour blend plus the displaced form.
The camera orbits the plane by cameraAngle at a downward tilt of cameraPitch. After every camera or viewport change, the plane is rescaled and repositioned to cover the camera's ground footprint (with a margin), sized to the footprint's bounding circle so the framing stays constant while orbiting. Because UV space maps to that footprint, frequency reads as a stable "waves on screen" count at any container size. The tilt has a dynamic floor: below a certain pitch (which depends on the field of view and aspect ratio) the top of the frame would rise above the horizon and no ground plane could cover it, so the pitch is clamped just above that point.
The GLSL simplex noise is webgl-noise by Ashima Arts (MIT).
