Skip to content

FreeIn reviewv1.0.0

Planet

A world: continents, ice, clouds that turn, city lights on the night side and a glowing atmosphere.
npx shadcn@latest add @vibebox/planet

Controls

Look
1
1
<Planet />

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

  1. 1Add the VibeBox registry to components.json:
    {
      "registries": {
        "@vibebox": "https://motion.vibeboxph.com/r/{name}.json"
      }
    }
  2. 2Add the component with the shadcn CLI:
    npx shadcn@latest add @vibebox/planet
  3. 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 files
components/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

PropTypeDefaultDescription
childrenReactNode—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).
speednumber1Multiplies the preset's speed; 0 holds the scene still.
intensitynumber1Multiplies the preset's brightness, 0.5 to 1.4.
seaLevelnumber0.52How much of the surface is ocean (0.3 to 0.7; each look sets its own).
cloudsnumber0.85How much cloud covers it (0 to 1; each look sets its own).
citiesnumber0.8How bright the city lights are at night (0 to 1.5; each look sets its own).
bandsnumber00 for a rocky world, 1 for bands of gas (0 to 1; each look sets its own).
sunAnglenumber0.9Where the sun is, in radians around the planet (-3 to 3; each look sets its own).
spinnumber1How fast it turns (0 to 3; each look sets its own).
interactivebooleantrueThe pointer turns and tilts the planet.
labelstring—Accessible name; leave it out when the scene is only a background.
stillbooleanfalseHold one composed frame until false (a gallery card that plays on hover).
classNamestring—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 label it 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 clouds to 0 there.
  • A liquid sphere of cyan, violet and magenta melting into a heart

    A liquid sphere that melts into the things you sell: fifteen shapes, four skins, tap for the next.

    3D · WebGL2

  • A glowing wireframe sphere with cyan and magenta lines and a halo of points

    A sphere drawn as light: glowing wire, bright nodes, a halo of points and a pulse you drive.

    3D · WebGL2

  • A glowing sphere of points in blue and magenta, turning into a ring

    A 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

↑ ↓ to move↵ to openesc to close