Three.js Solar System — Draggable Orbiting Planets in WebGL

Three.js Solar System · Animations · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Object3D pivot orbits: rotating an invisible parent sweeps each offset planet mesh through a perfect circle
Nested moon pivot: a second pivot inside the planet mesh orbits the moon while automatically following the planet
Dotted orbit path rings: 128-point THREE.Line circles trace every planet's orbit radius in world space
Tilted RingGeometry: a flat annulus parented to its planet renders a Saturn-like ring with DoubleSide shading
OrbitControls from a CDN: damped drag-to-rotate and bounded scroll-zoom camera with no bundler
Independent spin and orbit speeds: every planet rotates on its own axis at a different rate than it orbits
Real pause state: a single boolean halts orbital motion while camera controls keep responding
Emissive sun with an attached point light: illuminates every planet from a single moving light source

About this UI Snippet

How to Build an Orbiting Three.js Solar System With Object3D Pivots

Screenshot of the Three.js Solar System snippet rendered live

The Three.js Solar System snippet renders a glowing sun with six planets — one carrying a moon, one carrying a ring — all orbiting continuously at their own speeds, with a draggable camera and a pause button, using core Three.js plus its OrbitControls addon loaded from a CDN.

Object3D pivots do the orbit math, not manual trigonometry

The single most important idea in this snippet: each planet's mesh is offset along the X axis from an invisible THREE.Object3D pivot positioned at the origin, and the *pivot* is what actually gets rotated every frame — not the planet's own position. Because the mesh is a child of the pivot in the scene graph, rotating the parent automatically sweeps the offset child mesh through a perfect circular orbit. This completely replaces manually computing x = cos(angle) * radius, z = sin(angle) * radius by hand — Three.js's scene graph transform hierarchy does that math for free, and it's the standard technique for orbital motion in any 3D engine.

Moons nest a second pivot inside the planet

The planet with a moon carries a second Object3D pivot as a *child of the planet mesh itself*, with the moon offset from that inner pivot the same way the planet is offset from its own outer pivot. Rotating the inner pivot orbits the moon around the planet; because the moon-pivot is nested inside the already-orbiting planet, the moon automatically follows the planet around the sun too, with zero extra code to keep them together. This nested-pivot pattern is exactly how you'd extend the scene to add sub-moons, orbiting space stations, or any hierarchy of things-orbiting-things.

Dotted orbit paths use one Line per ring

Each planet's orbit radius is traced once at startup as a THREE.Line built from 128 points sampled around a circle — a static, unmoving ring drawn directly into world space (not attached to any pivot). This lets the visitor see exactly how far out each planet's orbit reaches even when the planet itself is hidden behind the sun or on the far side of the scene.

A ring is just a flat RingGeometry, tilted and parented to its planet

The outermost, ringed planet adds a THREE.RingGeometry — a flat annulus, not a torus — as a child of the planet mesh, tilted on its X axis and rendered with side: THREE.DoubleSide so it doesn't disappear when viewed edge-on or from below. Because it's parented to the planet mesh, the ring automatically follows the planet's orbit and spin without any additional transform logic.

Draggable camera via OrbitControls, bounded so you can't fly through the sun

A second CDN script, Three.js's OrbitControls addon, attaches drag-to-rotate and scroll-to-zoom camera behavior. minDistance and maxDistance keep the visitor from zooming in past the sun or zooming out until the whole system becomes a speck, and enableDamping makes every drag glide smoothly to a stop rather than snapping to a halt the instant the mouse button releases.

A real pause button, not just a visual toggle

Clicking "Pause orbits" sets a single boolean that the animation loop checks before advancing any rotation — the camera itself keeps responding to drags and OrbitControls' damping keeps updating even while paused, so the scene never feels frozen or unresponsive, only the orbital motion stops.

Where this technique scales

The pivot-per-orbiting-object pattern used here is the same one behind any "things circling other things" 3D scene — compare it with the flatter, ring-and-bead approach in orbit rings, or contrast the physically-accurate orbital hierarchy here against the purely decorative circular motion of a CSS-based orbiting icons snippet.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not have to reconstruct the pivot hierarchy in your head by reading the code top to bottom. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to draw out exactly which Object3D is a child of which, and why rotating a pivot moves its offset planet mesh in a circle without any manual trigonometry in the animation loop. The same assistant can help optimize it, for instance checking whether the six planet geometries and materials could share instances more aggressively, or whether the 128-point orbit-path lines could be simplified on lower-end devices. It is also useful for extending the scene: ask it to add an asteroid belt as a ring of small instanced meshes between two planets, make orbit speed scale with a slider instead of being fixed per planet, or add elliptical rather than perfectly circular orbits by offsetting each pivot's position slightly off-center. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build an animated "solar system" scene in plain HTML, CSS, and JavaScript using Three.js and its OrbitControls addon, both loaded from a CDN (no bundler, no build step).

Requirements:
- A full-viewport canvas with a WebGLRenderer sized to match it, updated on window resize including camera aspect ratio, plus OrbitControls with damping enabled and a bounded min/max zoom distance so the camera cannot pass through the sun or zoom out indefinitely.
- A central emissive sun mesh with an attached point light illuminating the rest of the scene, plus a low-intensity ambient light.
- At least six planets, each defined by its own distance from the sun, size, color, orbital speed, and self-rotation speed, stored in a single data array so new planets can be added by adding entries.
- For every planet, create an invisible Object3D "pivot" positioned at the scene origin, then offset the planet's mesh along one axis as a child of that pivot — orbital motion must come from rotating the pivot every frame, not from manually recalculating the planet's x/z position with trigonometry in the render loop.
- Give at least one planet a moon using a second, nested Object3D pivot that is itself a child of the planet's mesh (not the scene root), so rotating the planet's own orbit pivot automatically carries the moon along with it, while the moon's separate inner pivot rotation orbits it around the planet.
- Give at least one planet a flat ring using a RingGeometry parented to that planet's mesh, tilted on one axis, rendered with double-sided material so it remains visible from any viewing angle.
- Draw a static, non-rotating line tracing each planet's orbit radius directly in the scene (not attached to any pivot), built from a couple hundred points sampled evenly around a circle.
- Add a pause/resume button that toggles a boolean flag; when paused, all orbital rotation and self-spin must stop advancing, but the OrbitControls camera dragging and damping must continue to function normally.

Step by step

How to Use

  1. 1
    Load both CDN scriptsAdd three.min.js and OrbitControls.js from the CDN panel, in that order.
  2. 2
    Paste HTML, CSS, and JSThe sun and six planets appear immediately, orbiting at their own speeds.
  3. 3
    Drag to look aroundClick and drag to orbit the camera; scroll or pinch to zoom, bounded between the sun and the outer orbit.
  4. 4
    Pause and resumeClick the pause button to freeze all orbital motion; camera dragging still works while paused.
  5. 5
    Adjust planet dataEdit the PLANETS array to change distance, size, color, orbit speed, or add a moon or ring to any planet.
  6. 6
    Resize the windowRenderer size and camera aspect ratio update automatically.

Real-world uses

Common Use Cases

Teaching orbital mechanics or Object3D hierarchy
A concrete, visual demonstration of parent-child transforms — one of the most important concepts in any 3D scene graph.
Space, astronomy, and education sites
A draggable, explorable solar system fits science education platforms, planetariums, and space-themed products directly.
Portfolio and agency showpieces
Demonstrates real 3D scene-graph competence, not just a static render — a strong differentiator in a portfolio piece.
Game menu and loading backgrounds
An explorable orbital system gives a loading or menu screen something engaging to interact with while waiting.
Hierarchical data visualization
Repurpose the pivot-and-orbit pattern to visualize any parent-child dataset as orbiting bodies instead of a tree diagram.
Presentation and pitch-deck backgrounds
A slowly orbiting system makes a compelling, low-distraction animated background behind a fixed headline or logo.

Got questions?

Frequently Asked Questions

Each planet mesh is offset along the X axis from an invisible Object3D pivot located at the origin. Rotating that pivot every frame automatically sweeps the offset child mesh through a circular path, because Three.js applies the parent's rotation to all of its children. This replaces manual cos/sin position math with the scene graph's built-in transform hierarchy.

The moon has its own Object3D pivot, but that pivot is a child of the planet's mesh rather than the scene root. Because the moon-pivot inherits every transform applied to its parent planet, rotating the planet's own orbit pivot carries the moon along automatically, while independently rotating the inner moon-pivot orbits the moon around the planet.

The dotted orbit rings are static circles in world space — they never move — so they're added directly to the scene root rather than to any pivot. Keeping them separate from the animated pivots means they always show the true, unmoving orbit radius even while planets circle around them.

The ring is a THREE.RingGeometry, a flat annulus, added as a child of the planet mesh and tilted on its X axis. Its material sets side: THREE.DoubleSide because a flat ring viewed edge-on or from below would otherwise render as invisible — by default Three.js only draws the front face of a surface.

No. The pause button only stops the orbital rotation logic inside the animation loop. OrbitControls.update() is still called every frame regardless of the pause state, so dragging to look around and the damped deceleration after a drag both keep working normally.

Yes. Click JSX for a React component, Vue for a Vue 3 SFC, Angular for a standalone component, or Tailwind for a React + Tailwind CSS utility-class version. Build the pivots, planets, and controls inside a mount effect, keep the paused flag in a ref (not React state) so the animation loop can read it without triggering re-renders, and call controls.dispose() plus renderer.dispose() on cleanup.