About
Height is fractal noise sampled on the unit direction of the sphere, at two scales; the sea level cuts it into land and ocean, altitude blends lowland into highland and ice takes the poles. Clouds are a second, stretched noise that turns separately and shades the ground below it. Lighting is a plain diffuse term from one sun, a glint of the sun on open water, and on the dark side a hashed point lattice, masked by a low-frequency noise so cities cluster, lights up on land. The atmosphere is a power of the viewing angle at the limb, stronger on the sunward side, plus a glow just outside the disc.
Use it when
- A travel, education, science, climate or logistics page wants a world.
- A product with global reach wants a planet without a map.
- A game, space or sci-fi brand wants a backdrop.
Skip it when
- The page needs actual geography: use a map.
- A page that already has a 3D centrepiece.
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/planet - 3Use it:
import { Planet } from "@/components/vibebox/planet"; <Planet preset="earth" className="min-h-[560px] rounded-xl"> <div className="p-12 text-white"> <h1 className="text-4xl font-semibold">One world, turning.</h1> </div> </Planet>
Code
3 filescomponents/vibebox/planet/index.tsx
/**
* VibeBox Motion · Planet
* 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 { Planet, type PlanetProps, type PlanetColor, type PlanetValue } from "./planet.client";
export { PRESET_NAMES as PLANET_PRESETS, presets as planetPresets, type PlanetPreset } from "./planet.shader";components/vibebox/planet/planet.client.tsx
/**
* VibeBox Motion · Planet
* 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 { planetPiece, presets, type PlanetPreset } from "./planet.shader";
export type PlanetColor = "ocean" | "land" | "high" | "ice" | "cloud" | "atmos" | "city";
export type PlanetValue = "seaLevel" | "clouds" | "cities" | "bands" | "sunAngle" | "spin";
export type PlanetProps = {
/** What sits on the scene: a heading, a call to action, a whole section. */
children?: ReactNode;
/** A named look: `earth`, `mars`, `ice`, `gas`. */
preset?: PlanetPreset;
/** Overrides single palette colours with any CSS colour. */
colors?: Partial<Record<PlanetColor, 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<PlanetValue, number>>;
const POSTER: PosterSpec = { type: "orb", core: "land", edge: "ocean", halo: "atmos", size: 36 };
export function Planet({ children, preset, colors, speed, intensity, interactive = true, label, still, className, seaLevel, clouds, cities, bands, sunAngle, spin }: PlanetProps) {
return (
<BackdropCanvas
slug="planet"
piece={planetPiece}
presets={presets}
fallback="earth"
preset={preset}
colors={colors}
values={{ seaLevel, clouds, cities, bands, sunAngle, spin }}
speed={speed}
intensity={intensity}
poster={POSTER}
clearPoster
scale={0.8}
stillTime={3}
still={still}
interactive={interactive}
label={label}
className={className}
>
{children}
</BackdropCanvas>
);
}components/vibebox/planet/planet.shader.ts
/**
* VibeBox Motion · Planet
* 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 world: continents from fractal noise on the sphere, an ocean that shows depth, ice at the
* poles, a cloud deck that turns on its own, a night side that lights up with cities and an
* atmosphere that glows at the limb and scatters sunlight at the edge. One sphere, lit by one
* sun you can place; the pointer turns the planet. `bands` swaps the continents for a gas giant.
*/
const SCENE = /* glsl */ `
uniform vec3 uOcean;
uniform vec3 uLand;
uniform vec3 uHigh;
uniform vec3 uIce;
uniform vec3 uCloud;
uniform vec3 uAtmos;
uniform vec3 uCity;
uniform float uSeaLevel;
uniform float uClouds;
uniform float uCities;
uniform float uBands;
uniform float uSunAngle;
uniform float uSpin;
uniform float uSpeed;
uniform float uIntensity;
float T;
void main() {
vec2 uv = centred(gl_FragCoord.xy);
T = uTime * uSpeed;
float R = 0.36;
float r = length(uv);
vec3 col = vec3(0.0);
float alpha = 0.0;
vec3 sunDir = normalize(vec3(cos(uSunAngle), 0.28, sin(uSunAngle) + 0.55));
vec2 look = (uPointer - 0.5) * (0.35 + uHover * 0.8);
if (r < R * 1.18) {
float inside = step(r, R);
float z = sqrt(max(R * R - r * r, 0.0));
vec3 n = vec3(uv, z) / R;
float yaw = T * 0.05 * uSpin + look.x * 1.2;
float cy = cos(yaw), sy = sin(yaw);
float tilt = 0.38 - look.y * 0.6;
float ct = cos(tilt), st = sin(tilt);
mat3 ry = mat3(cy, 0.0, -sy, 0.0, 1.0, 0.0, sy, 0.0, cy);
mat3 rx = mat3(1.0, 0.0, 0.0, 0.0, ct, st, 0.0, -st, ct);
vec3 q = ry * rx * n;
float diffuse = clamp(dot(n, sunDir), 0.0, 1.0);
float lightAmt = smoothstep(-0.12, 0.35, dot(n, sunDir));
vec3 surface;
float landMask = 0.0;
if (uBands > 0.5) {
// A gas giant: bands of colour along the latitude, bent by turbulence.
float lat = q.y * 6.0 + 0.8 * (fbm3d(q * 2.6 + vec3(T * 0.02, 0.0, 0.0)) - 0.5) * 2.0;
float band = 0.5 + 0.5 * sin(lat * 2.0);
surface = mix(lin(uOcean), lin(uLand), band);
surface = mix(surface, lin(uHigh), smoothstep(0.55, 0.9, 0.5 + 0.5 * sin(lat * 5.0 + 1.3)) * 0.5);
} else {
float h = fbm3d(q * 3.1 + 3.0) * 0.78 + 0.22 * fbm3d(q * 9.0);
landMask = smoothstep(uSeaLevel, uSeaLevel + 0.025, h);
float alt = clamp((h - uSeaLevel) / (1.0 - uSeaLevel), 0.0, 1.0);
vec3 sea = mix(lin(uOcean) * 0.55, lin(uOcean) * 1.15, smoothstep(0.1, uSeaLevel, h));
vec3 ground = mix(lin(uLand), lin(uHigh), smoothstep(0.25, 0.8, alt));
surface = mix(sea, ground, landMask);
// Ice at the poles, creeping lower over high ground.
float ice = smoothstep(0.86, 0.95, abs(q.y) + 0.1 * (h - 0.5));
surface = mix(surface, lin(uIce), ice);
landMask *= 1.0 - ice;
}
// Clouds: a second noise that turns a little faster, and shadows the ground under it.
float cloud = 0.0;
if (uClouds > 0.001 && uBands < 0.5) {
vec3 cq = ry * rx * n;
vec3 cr = vec3(cq.x * cos(T * 0.012) - cq.z * sin(T * 0.012), cq.y, cq.x * sin(T * 0.012) + cq.z * cos(T * 0.012));
float c1 = fbm3d(cr * vec3(4.6, 8.0, 4.6) + 9.0);
cloud = smoothstep(0.55, 0.78, c1) * uClouds;
surface *= 1.0 - 0.28 * cloud;
}
vec3 lit = surface * (0.06 + 1.15 * diffuse);
lit = mix(lit, lin(uCloud) * (0.1 + 1.1 * diffuse), cloud * 0.92);
// Ocean glint toward the sun.
vec3 viewDir = vec3(0.0, 0.0, 1.0);
vec3 half_ = normalize(sunDir + viewDir);
float glint = pow(max(dot(n, half_), 0.0), 60.0) * (1.0 - landMask) * (1.0 - cloud) * lightAmt;
lit += vec3(1.0, 0.95, 0.85) * glint * 0.7;
// The night side: cities cluster on the land.
if (uCities > 0.001 && uBands < 0.5) {
float dark = 1.0 - lightAmt;
vec3 cp = q * 38.0;
float spots = step(0.9, h3(ivec3(floor(cp)))) * smoothstep(0.1, 0.0, length(fract(cp) - 0.5) - 0.18);
float clusters = smoothstep(0.55, 0.75, fbm3d(q * 6.0 + 20.0));
lit += lin(uCity) * spots * clusters * landMask * dark * (1.0 - cloud) * uCities * 2.2;
}
// The atmosphere at the limb, brightest on the lit side.
float limb = pow(1.0 - n.z, 2.6);
lit += lin(uAtmos) * limb * (0.25 + 1.4 * lightAmt);
col = lit * inside;
alpha = inside;
}
// The glow outside the limb.
if (uBands < 0.5 || true) {
float out_ = smoothstep(R, R * 1.18, r) * (1.0 - smoothstep(R * 1.18, R * 1.5, r));
float glow = exp(-(r - R) * 14.0) * step(R, r);
vec3 sunSide = vec3(0.0);
vec2 sd2 = normalize(vec2(cos(uSunAngle), 0.28));
float facing = 0.35 + 0.65 * max(dot(normalize(uv + vec2(0.0001)), sd2), 0.0);
sunSide = lin(uAtmos) * glow * facing * 0.8;
col += sunSide;
alpha = max(alpha, clamp(glow * facing * 0.8, 0.0, 1.0));
col += lin(uAtmos) * out_ * 0.0;
}
// A few distant stars behind, so the planet is somewhere.
vec2 g = uv * 70.0;
ivec2 id = ivec2(floor(g));
float star = step(0.985, h2(id)) * smoothstep(0.2, 0.0, length(fract(g) - 0.5 - (h22(id) - 0.5) * 0.5));
col += vec3(0.85, 0.9, 1.0) * star * (1.0 - alpha) * 0.9;
alpha = max(alpha, star * 0.9);
col *= uIntensity;
alpha = clamp(alpha, 0.0, 1.0);
fragColor = vec4(finish(col, gl_FragCoord.xy) * alpha, alpha);
}
`;
export const fragment = compose(SCENE);
export const PRESET_NAMES = ["earth", "mars", "ice", "gas"] as const;
export type PlanetPreset = (typeof PRESET_NAMES)[number];
export const presets: Record<PlanetPreset, ScenePreset> = {
earth: {
colors: { ocean: "#0d3d8a", land: "#3f8a4a", high: "#b59a6a", ice: "#f2f6ff", cloud: "#ffffff", atmos: "#6fb7ff", city: "#ffd27a" },
values: { seaLevel: 0.52, clouds: 0.85, cities: 0.8, bands: 0, sunAngle: 0.9, spin: 1, speed: 1, intensity: 1.05 },
},
mars: {
colors: { ocean: "#5a2a18", land: "#b4532a", high: "#e0a070", ice: "#f5ece4", cloud: "#f1d3bd", atmos: "#ff9a6a", city: "#ffd27a" },
values: { seaLevel: 0.4, clouds: 0.25, cities: 0, bands: 0, sunAngle: 0.8, spin: 1, speed: 1, intensity: 1.05 },
},
ice: {
colors: { ocean: "#2a6ab0", land: "#cfe3f5", high: "#ffffff", ice: "#ffffff", cloud: "#eaf4ff", atmos: "#b0dcff", city: "#bfe9ff" },
values: { seaLevel: 0.42, clouds: 0.5, cities: 0, bands: 0, sunAngle: 1.1, spin: 0.9, speed: 1, intensity: 1.05 },
},
gas: {
colors: { ocean: "#d9a066", land: "#f0d9b0", high: "#8a4a2a", ice: "#ffffff", cloud: "#ffffff", atmos: "#ffd9a0", city: "#ffffff" },
values: { seaLevel: 0.5, clouds: 0, cities: 0, bands: 1, sunAngle: 0.7, spin: 1.4, speed: 1, intensity: 1.05 },
},
};
export const planetPiece = createScenePiece("planet", fragment);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | What sits on the scene: a heading, a call to action, a whole section. |
| preset | "earth" | "mars" | "ice" | "gas" | "earth" | The look: earth, mars, ice, gas. |
| colors | { ocean?: string; land?: string; high?: string; ice?: string; cloud?: string; atmos?: string; city?: string } | — | Overrides single palette colours with any CSS colour: ocean (the sea (or a gas giant's dark bands)), land (lowland), high (highland (or a gas giant's accent bands)), ice (polar ice), cloud (clouds), atmos (the atmosphere's glow), city (city lights on the night side). |
| 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. |
| seaLevel | number | 0.52 | How much of the surface is ocean (0.3 to 0.7; each look sets its own). |
| clouds | number | 0.85 | How much cloud covers it (0 to 1; each look sets its own). |
| cities | number | 0.8 | How bright the city lights are at night (0 to 1.5; each look sets its own). |
| bands | number | 0 | 0 for a rocky world, 1 for bands of gas (0 to 1; each look sets its own). |
| sunAngle | number | 0.9 | Where the sun is, in radians around the planet (-3 to 3; each look sets its own). |
| spin | number | 1 | How fast it turns (0 to 3; each look sets its own). |
| interactive | boolean | true | The pointer turns and tilts the planet. |
| 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 planet; it does not turn and the clouds hold.
Edge cases and limits
- It is not a map: the continents are not real.
- It has a transparent background and a few stars; on a light page put it on a dark card.
- The cloud noise is a little costly on old phones; set
cloudsto 0 there.
Related

Morph Sphere
FreeA liquid sphere that melts into the things you sell: fifteen shapes, four skins, tap for the next.
3D · WebGL2

Wire Globe
FreeA sphere drawn as light: glowing wire, bright nodes, a halo of points and a pulse you drive.
3D · WebGL2

Particle Sphere
FreeA cloud of points that is a sphere until it is a ring, a heart, a cube, a helix or a galaxy, and shies from the pointer.
3D · three.js