About
A heightfield ray-marched per pixel. One octave loop reads a noise value two ways, as a smooth sum and as a creased sum, and ridge blends them; a power curve flattens the valleys; the water is the same field clamped at a level. Far ground takes fewer octaves, normalised so the level holds, which is most of the speed. Colour is altitude and slope: low ground in grass, steep faces in rock with faint strata, snow above a line that wavers with the surface, a lighter shore where land meets water. A short march toward the sun gives a soft shadow; fog gathers distance into haze. With glow above zero the water becomes a dark crust with hot veins that pulse.
Use it when
- A resort, trail, climbing, outdoor or property brand wants the place in the hero.
- A travel or adventure page wants a flight over somewhere without a photo.
- A logistics or energy page wants the volcanic or arctic look for contrast.
Skip it when
- The page is dense and technical; use Contour Lines or Dot Waves.
- The brand has nothing to do with places or the outdoors.
Install
- 1Add the VibeBox registry to
components.json:{ "registries": { "@vibebox": "https://motion.vibeboxph.com/r/{name}.json" } } - 2Add the component with the shadcn CLI:
npx shadcn@latest add @vibebox/terrain - 3Use it:
import { Terrain } from "@/components/vibebox/terrain"; <Terrain preset="alpine" className="min-h-[560px] rounded-xl"> <div className="p-12 text-white"> <h1 className="text-4xl font-semibold">Above the valley, before the day starts.</h1> </div> </Terrain>
Code
3 filescomponents/vibebox/terrain/index.tsx
/**
* VibeBox Motion · Terrain
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
export { Terrain, type TerrainProps, type TerrainColor, type TerrainValue } from "./terrain.client";
export { PRESET_NAMES as TERRAIN_PRESETS, presets as terrainPresets, type TerrainPreset } from "./terrain.shader";components/vibebox/terrain/terrain.client.tsx
/**
* VibeBox Motion · Terrain
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
"use client";
import type { ReactNode } from "react";
import { BackdropCanvas, type PosterSpec } from "@/lib/vibebox/backdrop-canvas";
import { terrainPiece, presets, type TerrainPreset } from "./terrain.shader";
export type TerrainColor = "haze" | "zenith" | "horizon" | "sun" | "grass" | "rock" | "snow" | "deep" | "shallow";
export type TerrainValue = "amp" | "ridge" | "power" | "water" | "snowLine" | "fog" | "sunAz" | "sunEl" | "glow" | "height";
export type TerrainProps = {
/** What sits on the scene: a heading, a call to action, a whole section. */
children?: ReactNode;
/** A named look: `alpine`, `tropical`, `desert`, `arctic`, `volcanic`, `dusk`. */
preset?: TerrainPreset;
/** Overrides single palette colours with any CSS colour. */
colors?: Partial<Record<TerrainColor, string>>;
/** Multiplies the preset's speed (1 keeps it, 0 holds the scene still). */
speed?: number;
/** Multiplies the preset's brightness. */
intensity?: number;
/** The pointer and touch move the scene. */
interactive?: boolean;
/** Accessible name; leave it out when the scene is only a background. */
label?: string;
/** Hold one still frame until this is false (for a gallery card). */
still?: boolean;
className?: string;
} & Partial<Record<TerrainValue, number>>;
const POSTER: PosterSpec = { type: "horizon", top: "zenith", horizon: "horizon", groundTop: "rock", groundBottom: "grass", at: 48, glow: "sun", glowAt: [72, 40] };
export function Terrain({ children, preset, colors, speed, intensity, interactive = true, label, still, className, amp, ridge, power, water, snowLine, fog, sunAz, sunEl, glow, height }: TerrainProps) {
return (
<BackdropCanvas
slug="terrain"
piece={terrainPiece}
presets={presets}
fallback="alpine"
preset={preset}
colors={colors}
values={{ amp, ridge, power, water, snowLine, fog, sunAz, sunEl, glow, height }}
speed={speed}
intensity={intensity}
poster={POSTER}
scale={0.6}
stillTime={5}
still={still}
interactive={interactive}
label={label}
className={className}
>
{children}
</BackdropCanvas>
);
}components/vibebox/terrain/terrain.shader.ts
/**
* VibeBox Motion · Terrain
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
import { compose, createScenePiece, type ScenePreset } from "@/lib/vibebox/engine/backdrops";
/*
* A flight over land. The ground is a fractal heightfield ray-marched in the fragment shader:
* valleys flattened by a power curve, ridges where the "ridge" value blends in creased noise, a
* water plane at a level you choose. Colour comes from altitude and slope (grass low, rock on
* steep faces, snow above a line), the sun lights it with a short soft shadow, and fog gathers the
* distance into the horizon. The camera glides forward on its own and leans toward the pointer.
*/
const SCENE = /* glsl */ `
uniform vec3 uZenith;
uniform vec3 uHorizon;
uniform vec3 uHaze;
uniform vec3 uSun;
uniform vec3 uGrass;
uniform vec3 uRock;
uniform vec3 uSnow;
uniform vec3 uDeep;
uniform vec3 uShallow;
uniform float uAmp;
uniform float uRidge;
uniform float uPower;
uniform float uWater;
uniform float uSnowLine;
uniform float uFog;
uniform float uSunAz;
uniform float uSunEl;
uniform float uGlow;
uniform float uHeight;
uniform float uSpeed;
uniform float uIntensity;
float T;
/* The heightfield: one octave loop feeds a smooth sum and a creased sum (the same noise read two
ways), blended by "ridge". Far ground takes fewer octaves, normalised so the level holds. */
float raw(vec2 p, int oct) {
vec2 q = p * 0.36 + 4.0;
float soft = 0.0, creased = 0.0, a = 0.5;
for (int i = 0; i < 5; i++) {
if (i >= oct) break;
float v = vnoise(q);
soft += a * v;
creased += a * (1.0 - abs(2.0 * v - 1.0));
q = ROT * q * 2.03 + 11.7;
a *= 0.5;
}
float norm = 1.0 / (1.0 - pow(0.5, float(oct)));
float h = mix(soft, creased * 0.92, uRidge) * norm;
return pow(clamp(h, 0.0, 1.0), uPower) * uAmp;
}
float ground(vec2 p, int oct) { return max(raw(p, oct), uWater); }
vec3 skyColor(vec3 rd, vec3 L) {
float y = clamp(rd.y, 0.0, 1.0);
vec3 col = mix(lin(uHorizon), lin(uZenith), pow(y, 0.55));
float s = max(dot(rd, L), 0.0);
col += lin(uSun) * (pow(s, 5.0) * 0.14 + pow(s, 90.0) * 0.55 + pow(s, 1500.0) * 5.0);
return col;
}
void main() {
vec2 uv = centred(gl_FragCoord.xy);
T = uTime * uSpeed;
float lean = 0.3 + uHover;
vec3 L = normalize(vec3(sin(uSunAz) * cos(uSunEl), sin(uSunEl), cos(uSunAz) * cos(uSunEl)));
// The camera: forward over the land, wandering a little, never below the ground.
vec3 ro = vec3((uPointer.x - 0.5) * 0.6 * lean + sin(T * 0.06) * 0.7, 0.0, T * 0.32);
ro.y = max(ground(ro.xz, 5) + 0.1, uWater + 0.1) + uHeight;
float yaw = sin(T * 0.045) * 0.22 + (uPointer.x - 0.5) * 0.3 * lean;
float pitch = -0.07 - (uPointer.y - 0.5) * 0.14 * lean;
vec3 f = normalize(vec3(sin(yaw), sin(pitch), cos(yaw)));
vec3 r = normalize(cross(vec3(0.0, 1.0, 0.0), f));
vec3 u = cross(f, r);
vec3 rd = normalize(f + (uv.x * r + uv.y * u) * 1.2);
vec3 fogCol = mix(lin(uHaze), lin(uSun) * 0.7, pow(max(dot(rd, L), 0.0), 7.0) * 0.45);
vec3 col;
float t = 0.03;
float hit = -1.0;
bool lookingAway = rd.y > 0.03 && ro.y > uAmp;
if (!lookingAway) {
for (int i = 0; i < 76; i++) {
vec3 p = ro + rd * t;
int oct = t < 1.6 ? 5 : (t < 4.0 ? 4 : (t < 9.0 ? 3 : 2));
float d = p.y - ground(p.xz, oct);
if (d < 0.0012 * t) { hit = t; break; }
t += clamp(d * 0.45, 0.003 * t, 0.7);
if (t > 20.0) break;
}
}
if (hit < 0.0) {
col = mix(skyColor(rd, L), fogCol, pow(1.0 - clamp(rd.y * 2.2, 0.0, 1.0), 3.0) * 0.8);
} else {
vec3 p = ro + rd * hit;
float rh = raw(p.xz, 5);
float e = 0.004 + 0.003 * hit;
float h0 = ground(p.xz, 5);
vec3 n = normalize(vec3(h0 - ground(p.xz + vec2(e, 0.0), 5), e * 2.2, h0 - ground(p.xz + vec2(0.0, e), 5)));
bool water = rh <= uWater + 0.0015;
if (water && uGlow > 0.01) {
// Lava: a dark crust broken by hot veins that pulse, drawn on a warped field so they curve.
vec2 lp = p.xz * 2.6 + vec2(T * 0.03, -T * 0.02);
lp += 0.55 * vec2(gnoise(lp * 0.8), gnoise(lp * 0.8 + 9.0));
float vein = 1.0 - abs(gnoise(lp * 1.3));
float vein2 = 1.0 - abs(gnoise(lp * 3.1 + 7.0));
float cracks = pow(clamp(vein, 0.0, 1.0), 14.0) + 0.5 * pow(clamp(vein2, 0.0, 1.0), 16.0);
float pulse = 0.8 + 0.2 * sin(T * 1.3 + gnoise(lp) * 6.0);
float crustT = 0.5 + 0.5 * gnoise(lp * 0.6 + 3.0);
vec3 crust = mix(lin(uRock) * 0.35, lin(uDeep) * 0.55, crustT * crustT);
vec3 hotCol = mix(lin(uDeep) * 2.0, lin(uShallow) * 2.2, clamp(cracks, 0.0, 1.0));
col = mix(crust, hotCol * pulse, clamp(cracks * 1.4, 0.0, 1.0));
col *= 0.7 + 0.5 * uGlow;
} else if (water) {
float depth = clamp((uWater - rh) / 0.35, 0.0, 1.0);
vec3 wn = normalize(vec3(gnoise(p.xz * 5.0 + vec2(T * 0.3, 0.0)) * 0.07, 1.0, gnoise(p.xz * 5.0 + vec2(9.0, -T * 0.25)) * 0.07));
vec3 refl = reflect(rd, wn);
refl.y = abs(refl.y);
float fres = 0.03 + 0.97 * pow(1.0 - max(dot(-rd, wn), 0.0), 5.0);
vec3 wcol = mix(lin(uShallow), lin(uDeep), smoothstep(0.0, 0.9, depth));
col = mix(wcol, skyColor(refl, L), fres);
float foam = smoothstep(0.1, 0.0, depth) * (0.4 + 0.6 * vnoise(p.xz * 14.0 - T * 0.3));
col = mix(col, vec3(0.9), foam * 0.35);
} else {
float alt = (rh - uWater) / max(uAmp - uWater, 0.001);
// A little grain on the surface so near ground is not smooth clay.
float near = 1.0 - smoothstep(3.0, 10.0, hit);
vec2 bp = p.xz * 38.0;
n = normalize(n + vec3(gnoise(bp), 0.0, gnoise(bp + 17.0)) * 0.07 * near);
float slope = 1.0 - n.y;
float detail = vnoise(p.xz * 9.0) * 0.5 + vnoise(p.xz * 27.0) * 0.25;
float patches = vnoise(p.xz * 3.2) * 0.6 + vnoise(p.xz * 11.0) * 0.4;
vec3 grass = mix(lin(uGrass) * 0.5, lin(uGrass) * 1.2, smoothstep(0.35, 0.65, patches)) * (0.8 + 0.4 * detail);
float strata = 0.82 + 0.36 * vnoise(vec2(p.y * 26.0, (p.x + p.z) * 2.5));
vec3 rock = lin(uRock) * (0.7 + 0.6 * detail) * strata;
vec3 snow = lin(uSnow);
float rockiness = smoothstep(0.1, 0.38, slope + alt * 0.25);
vec3 alb = mix(grass, rock, rockiness);
float snowMask = smoothstep(uSnowLine, uSnowLine + 0.12, alt + (n.y - 0.6) * 0.25 + detail * 0.1);
alb = mix(alb, snow, snowMask);
// A shore of lighter ground where the land meets the water.
alb = mix(alb, mix(grass, snow, 0.35), smoothstep(0.035, 0.0, alt) * 0.55);
float diff = max(dot(n, L), 0.0);
// A short, soft shadow marched toward the sun.
float shade = 1.0;
for (int k = 1; k <= 4; k++) {
vec3 s = p + n * 0.01 + L * (float(k) * 0.15);
float d = s.y - ground(s.xz, 2);
shade = min(shade, clamp(7.0 * d / (float(k) * 0.15), 0.0, 1.0));
}
vec3 amb = mix(lin(uHorizon), lin(uZenith), 0.4) * (0.62 + 0.5 * n.y);
col = alb * (lin(uSun) * diff * shade * 1.35 + amb);
// The lava's glow on low ground.
col += lin(uShallow) * uGlow * 0.1 * smoothstep(0.3, 0.0, alt) * (0.4 + 0.6 * n.y);
}
float fogAmt = 1.0 - exp(-(hit * hit * 0.007 + hit * 0.03) * uFog);
col = mix(col, fogCol, clamp(fogAmt, 0.0, 1.0));
}
col *= uIntensity;
col *= 1.0 - 0.28 * smoothstep(0.5, 1.15, length(uv * vec2(0.85, 1.1)));
fragColor = vec4(finish(col, gl_FragCoord.xy), 1.0);
}
`;
export const fragment = compose(SCENE);
export const PRESET_NAMES = ["alpine", "tropical", "desert", "arctic", "volcanic", "dusk"] as const;
export type TerrainPreset = (typeof PRESET_NAMES)[number];
export const presets: Record<TerrainPreset, ScenePreset> = {
alpine: {
colors: { haze: "#cfe0ee", zenith: "#356fc2", horizon: "#cfe2f2", sun: "#fff3d8", grass: "#4c7a3e", rock: "#7b7366", snow: "#f6f8fc", deep: "#0f3a58", shallow: "#3a9aa8" },
values: { amp: 2.4, ridge: 0.72, power: 1.45, water: 0.34, snowLine: 0.46, fog: 0.5, sunAz: 0.7, sunEl: 0.5, glow: 0, height: 0.35, speed: 0.6, intensity: 1 },
},
tropical: {
colors: { haze: "#cfeaf0", zenith: "#2a86d6", horizon: "#c4e8f0", sun: "#fffbe8", grass: "#2f8f4e", rock: "#8a7a5c", snow: "#f1e7c8", deep: "#06476e", shallow: "#2ed0c0" },
values: { amp: 1.1, ridge: 0.2, power: 1.9, water: 0.34, snowLine: 3, fog: 0.4, sunAz: -0.5, sunEl: 0.8, glow: 0, height: 0.2, speed: 0.55, intensity: 1.05 },
},
desert: {
colors: { haze: "#f4d9b0", zenith: "#4f86c8", horizon: "#fbe0b6", sun: "#fff4d6", grass: "#d6a066", rock: "#9c4f2c", snow: "#f6e0b6", deep: "#1f5f78", shallow: "#6cc1b0" },
values: { amp: 1.4, ridge: 0.8, power: 1.2, water: -1, snowLine: 3, fog: 0.6, sunAz: 0.9, sunEl: 0.45, glow: 0, height: 0.14, speed: 0.6, intensity: 1 },
},
arctic: {
colors: { haze: "#e0edf8", zenith: "#4b7fb8", horizon: "#e2eff9", sun: "#ffffff", grass: "#a9c6dc", rock: "#6f879d", snow: "#f7fafe", deep: "#0a3b5c", shallow: "#6fb6d8" },
values: { amp: 1.8, ridge: 0.6, power: 1.4, water: 0.24, snowLine: 0.3, fog: 0.7, sunAz: 0.5, sunEl: 0.3, glow: 0, height: 0.15, speed: 0.5, intensity: 1 },
},
volcanic: {
colors: { haze: "#4a2418", zenith: "#3a1420", horizon: "#ff8a45", sun: "#ffd29a", grass: "#4a3a36", rock: "#33262a", snow: "#6a5048", deep: "#c01c00", shallow: "#ffb52a" },
values: { amp: 2, ridge: 0.85, power: 1.3, water: 0.85, snowLine: 3, fog: 0.7, sunAz: 0.9, sunEl: 0.12, glow: 1, height: 0.3, speed: 0.5, intensity: 1.1 },
},
dusk: {
colors: { haze: "#e8a58f", zenith: "#26357a", horizon: "#ff9e7a", sun: "#ffd7a0", grass: "#3a5578", rock: "#5b4868", snow: "#ffd9d0", deep: "#16284f", shallow: "#c46a8a" },
values: { amp: 2.2, ridge: 0.65, power: 1.45, water: 0.26, snowLine: 0.58, fog: 0.5, sunAz: 1.15, sunEl: 0.18, glow: 0, height: 0.25, speed: 0.55, intensity: 1.15 },
},
};
export const terrainPiece = createScenePiece("terrain", fragment);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | What sits on the scene: a heading, a call to action, a whole section. |
| preset | "alpine" | "tropical" | "desert" | "arctic" | "volcanic" | "dusk" | "alpine" | The look: alpine, tropical, desert, arctic, volcanic, dusk. |
| colors | { haze?: string; zenith?: string; horizon?: string; sun?: string; grass?: string; rock?: string; snow?: string; deep?: string; shallow?: string } | — | Overrides single palette colours with any CSS colour: haze (the distance fog's colour), zenith (the sky overhead), horizon (the sky at the horizon), sun (the sun and its light on the land), grass (the low ground (grass, sand, ice or ash)), rock (steep faces and cliffs), snow (high ground above the snow line), deep (deep water (or the lava's dark red)), shallow (shallow water (or the lava's bright yellow)). |
| speed | number | 1 | Multiplies the preset's speed; 0 holds the scene still. |
| intensity | number | 1 | Multiplies the preset's brightness, 0.5 to 1.4. |
| amp | number | 2.4 | How tall the land stands (0.8 to 3; each look sets its own). |
| ridge | number | 0.72 | 0 for rolling land, 1 for creased peaks (0 to 1; each look sets its own). |
| power | number | 1.45 | How flat the low ground is (higher is flatter) (0.9 to 2.4; each look sets its own). |
| water | number | 0.34 | Where the water plane sits; below the land to have none (-1 to 1.4; each look sets its own). |
| snowLine | number | 0.46 | How high the snow starts (3 for no snow) (0.1 to 3; each look sets its own). |
| fog | number | 0.5 | How fast the distance goes to haze (0.1 to 1.6; each look sets its own). |
| sunAz | number | 0.7 | Where the sun sits around the compass (-3.1 to 3.1; each look sets its own). |
| sunEl | number | 0.5 | How high the sun stands (0.04 to 1.2; each look sets its own). |
| glow | number | 0 | 0 for water, above 0 the water is molten (0 to 1.5; each look sets its own). |
| height | number | 0.35 | How high the camera flies above the land (0.05 to 1; each look sets its own). |
| interactive | boolean | true | The camera leans toward the pointer and swings a little further when it is over the scene. |
| label | string | — | Accessible name; leave it out when the scene is only a background. |
| still | boolean | false | Hold one composed frame until false (a gallery card that plays on hover). |
| className | string | — | Merged with cn(); give it a size and a radius. |
Accessibility and motion
- Keyboard
- Not focusable; content on it keeps its own focus order.
- Screen readers
- Decorative by default; with
labelit is an image with that name. - Touch
- A finger leans the camera like the cursor does; scrolling is never blocked.
- Reduced motion
- One still frame of the land; the camera does not fly and the lava does not pulse.
Edge cases and limits
- It is the heaviest background in the library: the stage settles to a still frame on slower phones.
- The sun and snow are the brightest parts: keep text away from them or on a card.
- A very low sun (dusk, volcanic) leaves the near ground in shadow by design.
Related

Ridgeline
FreeLayers of mountains, dunes or forest receding into haze under a low sun.
Backgrounds · WebGL2

Aurora
FreeCurtains of northern light over a night sky and a mountain ridge, drawn toward the pointer.
Backgrounds · WebGL2

Bokeh
FreeOut-of-focus lights in three depths, brighter at the rim and drifting with the pointer.
Backgrounds · WebGL2