About
A skyline is a fractal curve of x. Each layer takes one, smooth fractal for dunes and ridged noise for peaks, mixes between them with peak, and adds a fringe of triangles for pines on the nearer layers. Layers are painted far to near, tinted along the far-to-near ramp and veiled in mist that is thickest in the far ones and measured from the layer's baseline, so it never inherits the skyline's jaggedness. A sunlit rim catches the edges that face the sun; snow brightens the tallest tops.
Use it when
- A property developer, resort, trail, hotel or outdoor brand wants a place to sit behind its promise.
- A creative studio wants a calm illustrated hero without commissioning one.
- A food or farm brand wants the forest or the dunes.
Skip it when
- The page needs a literal map or a photo of the real place.
- The brand is dense and technical; use Contour Lines.
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/ridgeline - 3Use it:
import { Ridgeline } from "@/components/vibebox/ridgeline"; <Ridgeline preset="alpine" className="min-h-[560px] rounded-xl"> <div className="p-12 text-white"> <h1 className="text-4xl font-semibold">Land worth the long way round.</h1> </div> </Ridgeline>
Code
3 filescomponents/vibebox/ridgeline/index.tsx
/**
* VibeBox Motion · Ridgeline
* 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 { Ridgeline, type RidgelineProps, type RidgelineColor, type RidgelineValue } from "./ridgeline.client";
export { PRESET_NAMES as RIDGELINE_PRESETS, presets as ridgelinePresets, type RidgelinePreset } from "./ridgeline.shader";components/vibebox/ridgeline/ridgeline.client.tsx
/**
* VibeBox Motion · Ridgeline
* 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 { ridgelinePiece, presets, type RidgelinePreset } from "./ridgeline.shader";
export type RidgelineColor = "sky" | "horizon" | "sun" | "far" | "near" | "mist";
export type RidgelineValue = "layers" | "peak" | "trees" | "snow" | "sunX" | "sunY";
export type RidgelineProps = {
/** What sits on the scene: a heading, a call to action, a whole section. */
children?: ReactNode;
/** A named look: `alpine`, `dunes`, `forest`, `paper`, `mist`, `volcano`. */
preset?: RidgelinePreset;
/** Overrides single palette colours with any CSS colour. */
colors?: Partial<Record<RidgelineColor, 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<RidgelineValue, number>>;
const POSTER: PosterSpec = { type: "horizon", top: "sky", horizon: "horizon", groundTop: "far", groundBottom: "near", at: 55, glow: "sun", glowAt: [66, 50] };
export function Ridgeline({ children, preset, colors, speed, intensity, interactive = true, label, still, className, layers, peak, trees, snow, sunX, sunY }: RidgelineProps) {
return (
<BackdropCanvas
slug="ridgeline"
piece={ridgelinePiece}
presets={presets}
fallback="alpine"
preset={preset}
colors={colors}
values={{ layers, peak, trees, snow, sunX, sunY }}
speed={speed}
intensity={intensity}
poster={POSTER}
scale={0.5}
stillTime={3}
still={still}
interactive={interactive}
label={label}
className={className}
>
{children}
</BackdropCanvas>
);
}components/vibebox/ridgeline/ridgeline.shader.ts
/**
* VibeBox Motion · Ridgeline
* 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";
/*
* Layers of land receding into haze: mountains, dunes, hills or a forest edge, painted far to near
* under a sky with a sun. Each layer's skyline is a fractal curve (smooth for dunes, creased for
* peaks), tinted from the far colour to the near colour and veiled in mist where it meets the
* layer behind. The pointer lets the layers slide a little at different speeds.
*/
const SCENE = /* glsl */ `
uniform vec3 uSky;
uniform vec3 uHorizon;
uniform vec3 uSun;
uniform vec3 uFar;
uniform vec3 uNear;
uniform vec3 uMist;
uniform float uLayers;
uniform float uPeak;
uniform float uTrees;
uniform float uSnow;
uniform float uSunX;
uniform float uSunY;
uniform float uSpeed;
uniform float uIntensity;
float T;
float skyline(float x, float t, float seed) {
float smoothHills = fbm3(vec2(x * (0.9 + t * 0.9), seed));
float creased = ridged(vec2(x * (0.7 + t * 0.8), seed + 3.0));
float h = mix(smoothHills, creased * 0.9, uPeak);
return (h - 0.5) * (0.5 + 0.55 * t);
}
void main() {
vec2 uv = centred(gl_FragCoord.xy);
T = uTime * uSpeed;
float aspect = uResolution.x / uResolution.y;
float shift = (uPointer.x - 0.5) * 0.12 * (0.3 + uHover);
// Sky and sun.
float sy = clamp((uv.y + 0.5), 0.0, 1.0);
vec3 col = lin(mix(uHorizon, uSky, smoothstep(0.2, 1.0, sy)));
vec2 sunPos = vec2(uSunX * 0.5 * aspect, -0.02 + uSunY * 0.45);
float sd = length(uv - sunPos);
vec3 sun = lin(uSun);
col += sun * (exp(-sd * 3.2) * 0.35 + exp(-sd * 11.0) * 0.5 + 2.2 * smoothstep(0.05, 0.035, sd));
int count = int(clamp(uLayers, 2.0, 7.0));
for (int i = 0; i < 7; i++) {
if (i >= count) break;
float t = float(i) / float(max(count - 1, 1));
float x = (uv.x + shift * (0.4 + 1.6 * t)) * 1.1 + T * 0.006 * (0.4 + t) + float(i) * 11.0;
float baseline = mix(0.02, -0.3, t);
float amp = skyline(x, t, float(i) * 7.3);
// The ridge itself, smooth; the tree fringe rides on it and only touches the silhouette.
float ridge = baseline + amp * mix(0.55, 1.0, uPeak * 0.5 + 0.5);
float top = ridge;
// Pines: a fringe of soft triangles on the nearer layers, grouped in stands rather than a comb.
if (uTrees > 0.01 && t > 0.25) {
float f = x * (34.0 + 46.0 * t);
float tri = abs(fract(f) - 0.5) * 2.0;
float stand = 0.55 + 0.45 * vnoise(vec2(f * 0.11, float(i) * 3.7));
float tall = stand * (0.7 + 0.3 * h2(ivec2(int(floor(f)), i)));
top += uTrees * (0.026 + 0.022 * t) * pow(1.0 - tri, 1.35) * tall;
}
float px = 1.4 / uResolution.y;
float inside = smoothstep(top + px, top - px, uv.y);
if (inside <= 0.0) continue;
vec3 layerCol = lin(mix(uFar, uNear, pow(t, 0.9)));
// Mist pools toward the top edge of each layer, thickest in the far ones.
// Measured from the layer's own baseline, so the mist never inherits the skyline's jaggedness.
float depthFromTop = clamp((baseline + 0.2 - uv.y) / 0.42, 0.0, 1.0);
float mist = (1.0 - depthFromTop) * (0.85 - 0.6 * t) + (1.0 - t) * 0.12;
layerCol = mix(layerCol, lin(uMist), clamp(mist, 0.0, 1.0));
// Sunlit rim on the side facing the sun.
float rim = smoothstep(0.012, 0.0, ridge - uv.y) * (0.25 + 0.75 * smoothstep(0.4, -0.2, uv.x - sunPos.x));
layerCol += sun * rim * 0.28 * (1.0 - t * 0.5);
// Snow where the peaks are tallest.
if (uSnow > 0.01) {
float line = baseline + 0.08 + (1.0 - uSnow) * 0.08;
float depth = 0.045 + 0.07 * vnoise(vec2(x * 6.0, float(i) * 5.1));
float snow = smoothstep(line, line + 0.06, ridge) * smoothstep(depth, 0.0, ridge - uv.y);
layerCol = mix(layerCol, lin(mix(uMist, uSun, 0.45)), snow * 0.9);
}
col = mix(col, layerCol, inside);
}
col *= uIntensity;
col *= 1.0 - 0.25 * smoothstep(0.55, 1.2, length(uv * vec2(0.85, 1.05)));
fragColor = vec4(finish(col, gl_FragCoord.xy), 1.0);
}
`;
export const fragment = compose(SCENE);
export const PRESET_NAMES = ["alpine", "dunes", "forest", "paper", "mist", "volcano"] as const;
export type RidgelinePreset = (typeof PRESET_NAMES)[number];
export const presets: Record<RidgelinePreset, ScenePreset> = {
alpine: {
colors: { sky: "#2d4a8a", horizon: "#ffd6b8", sun: "#fff0d0", far: "#a9b4d6", near: "#141c36", mist: "#f1d9d2" },
values: { layers: 6, peak: 1, trees: 0.12, snow: 0.7, sunX: 0.35, sunY: 0.12, speed: 0.6, intensity: 1 },
},
dunes: {
colors: { sky: "#e8864a", horizon: "#ffd9a0", sun: "#fff5dc", far: "#f2b077", near: "#5a2418", mist: "#ffe0b8" },
values: { layers: 5, peak: 0, trees: 0, snow: 0, sunX: -0.3, sunY: 0.2, speed: 0.5, intensity: 1 },
},
forest: {
colors: { sky: "#7fa6a0", horizon: "#e9f0d8", sun: "#fff8e0", far: "#a8c2a6", near: "#0d2a1f", mist: "#e4efdc" },
values: { layers: 7, peak: 0.35, trees: 1, snow: 0, sunX: 0.4, sunY: 0.3, speed: 0.5, intensity: 1 },
},
paper: {
colors: { sky: "#ffd9a8", horizon: "#fff0d8", sun: "#fffaf0", far: "#f7a98b", near: "#33577a", mist: "#ffe9d1" },
values: { layers: 5, peak: 0.25, trees: 0, snow: 0, sunX: 0.3, sunY: 0.28, speed: 0.4, intensity: 1 },
},
mist: {
colors: { sky: "#c9d4df", horizon: "#f1f4f4", sun: "#ffffff", far: "#cfd9df", near: "#52606c", mist: "#eef2f3" },
values: { layers: 6, peak: 0.6, trees: 0.5, snow: 0, sunX: 0.1, sunY: 0.3, speed: 0.4, intensity: 1 },
},
volcano: {
colors: { sky: "#1a0b16", horizon: "#ff6a33", sun: "#ffc27a", far: "#6d2a2a", near: "#0b0508", mist: "#ff8a52" },
values: { layers: 5, peak: 0.9, trees: 0, snow: 0, sunX: 0.0, sunY: 0.1, speed: 0.5, intensity: 1 },
},
};
export const ridgelinePiece = createScenePiece("ridgeline", fragment);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | What sits on the scene: a heading, a call to action, a whole section. |
| preset | "alpine" | "dunes" | "forest" | "paper" | "mist" | "volcano" | "alpine" | The look: alpine, dunes, forest, paper, mist, volcano. |
| colors | { sky?: string; horizon?: string; sun?: string; far?: string; near?: string; mist?: string } | — | Overrides single palette colours with any CSS colour: sky (the sky overhead), horizon (the sky at the horizon), sun (the sun and the rim light on the ridges), far (the farthest ridge), near (the nearest ridge), mist (the mist between layers). |
| 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. |
| layers | number | 6 | How many ridges (2 to 7) (2 to 7; each look sets its own). |
| peak | number | 1 | 0 for rolling hills and dunes, 1 for sharp peaks (0 to 1; each look sets its own). |
| trees | number | 0.35 | A fringe of pines on the nearer ridges (0 to 1; each look sets its own). |
| snow | number | 0.7 | Snow on the tallest peaks (0 to 1; each look sets its own). |
| sunX | number | 0.35 | Where the sun sits across the frame (-1 to 1; each look sets its own). |
| sunY | number | 0.12 | How high the sun stands (0 to 1; each look sets its own). |
| interactive | boolean | true | The pointer slides the layers, the near ones further. |
| 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 moves the scene like the cursor does; scrolling is never blocked.
- Reduced motion
- One still frame of the landscape; the layers do not drift.
Edge cases and limits
- The sun and its glow are the bright spot: put copy away from them.
- Seven layers on a small phone is fine, but the far layers are only a few pixels tall; use 4 or 5.
- Pines look best at
trees0.5 to 1 on the forest and mist looks.
Related

Terrain
FreeA flight over land: mountains, dunes, islands, ice or lava, with fog, a sun and a camera that leans to the pointer.
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