← Illustrations

Solar System

Eight planets circle a glowing sun on tilted orbits, each at its own speed: Mercury whips round while Neptune barely moves. Planets pass behind the sun on the far side, their night sides face away from it, Saturn has its rings and the Earth its Moon.

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-solar-system> where the scene should go. It fills the element with an SVG scene.

Options

Add any of these attributes to the same element.

data-speedSpeed multiplier. At 1 Mercury goes round in 5 seconds. Default 1.
data-orbits="false"Hide the orbit lines.
data-stars="false"No stars.

It is 8:5 with a deep-space background by default; set your own height or background on the element to change that. Orbit speeds follow Kepler’s law; sizes and distances are not to scale. It pauses when off screen. With reduced motion it’s a still picture.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Solar System: eight planets circle a glowing sun on tilted orbits, each
    // at its own speed (the inner ones much faster, as in the real thing).
    // Planets pass behind the sun on the far side, the night side of each
    // faces away from it, Saturn has its rings and the Earth its Moon. Drawn
    // as SVG that fills the element, and paused when off screen.
    // Usage: <div data-solar-system></div>
    // It is 8:5 by default with a deep-space background; give it your own
    // height or background to change that.
    // Optional: data-speed (default 1; Mercury goes round in 5 s at 1),
    // data-orbits="false" (no orbit lines), data-stars="false" (no stars).
    var DEFAULTS = { speed: 1 };
    var NS = 'http://www.w3.org/2000/svg';
    var uid = 0;
    var CX = 200, CY = 125, TILT = 0.34, YEAR = 5000;

    // Orbit radius, planet radius, colour, highlight.
    var PLANETS = [
      { name: 'mercury', a: 34, r: 2.3, col: '#a8a29e', hi: '#e7e5e4' },
      { name: 'venus', a: 50, r: 3.5, col: '#f59e0b', hi: '#fde68a' },
      { name: 'earth', a: 68, r: 3.9, col: '#2563eb', hi: '#7dd3fc', moon: true },
      { name: 'mars', a: 87, r: 3, col: '#dc2626', hi: '#fca5a5' },
      { name: 'jupiter', a: 113, r: 8.5, col: '#c2410c', hi: '#fed7aa', bands: true },
      { name: 'saturn', a: 139, r: 6.4, col: '#ca8a04', hi: '#fef08a', ring: true },
      { name: 'uranus', a: 162, r: 4.6, col: '#06b6d4', hi: '#a5f3fc' },
      { name: 'neptune', a: 184, r: 4.4, col: '#4f46e5', hi: '#a5b4fc' }
    ];

    var css =
      '[data-solar-system]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5;' +
      'background:radial-gradient(ellipse at 50% 50%,#1e1b4b 0%,#0f0c29 55%,#05040f 100%)}' +
      '.sls-svg{position:absolute;inset:0;width:100%;height:100%;display:block;overflow:visible}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function build(el) {
      var d = el.dataset;
      var n = ++uid;
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'The planets orbiting the sun');
      if (!el.hasAttribute('role')) el.setAttribute('role', 'img');

      var g = 'sls' + n;
      var defs = '<radialGradient id="' + g + '-glow"><stop offset="0" stop-color="#fde68a" stop-opacity=".7"/><stop offset=".35" stop-color="#f59e0b" stop-opacity=".25"/><stop offset="1" stop-color="#f59e0b" stop-opacity="0"/></radialGradient>' +
        '<radialGradient id="' + g + '-sun" cx=".4" cy=".4"><stop offset="0" stop-color="#fffbeb"/><stop offset=".45" stop-color="#fde047"/><stop offset="1" stop-color="#f97316"/></radialGradient>' +
        // The night side: dark on the half facing +x, rotated away from the sun.
        '<linearGradient id="' + g + '-night"><stop offset=".42" stop-color="#05040f" stop-opacity="0"/><stop offset=".7" stop-color="#05040f" stop-opacity=".75"/></linearGradient>';
      PLANETS.forEach(function (p) {
        defs += '<radialGradient id="' + g + '-' + p.name + '" cx=".35" cy=".35" r=".75"><stop offset="0" stop-color="' + p.hi + '"/><stop offset="1" stop-color="' + p.col + '"/></radialGradient>';
      });

      var stars = '';
      if (d.stars !== 'false') {
        for (var i = 0; i < 90; i++) {
          stars += '<circle class="sls-star" cx="' + (Math.random() * 1200 - 400).toFixed(1) + '" cy="' + (Math.random() * 500 - 125).toFixed(1) + '" r="' + (Math.random() * 0.8 + 0.3).toFixed(2) + '" fill="#fff" opacity="' + (Math.random() * 0.6 + 0.2).toFixed(2) + '"/>';
        }
      }
      var orbits = '';
      if (d.orbits !== 'false') {
        PLANETS.forEach(function (p) {
          orbits += '<ellipse cx="' + CX + '" cy="' + CY + '" rx="' + p.a + '" ry="' + (p.a * TILT).toFixed(2) + '" fill="none" stroke="#c7d2fe" stroke-opacity=".16" stroke-width=".8"/>';
        });
      }

      el.innerHTML =
        '<svg class="sls-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
        '<defs>' + defs + '</defs>' + stars + orbits +
        '<g class="sls-back"></g>' +
        '<circle class="sls-corona" cx="' + CX + '" cy="' + CY + '" r="54" fill="url(#' + g + '-glow)"/>' +
        '<circle cx="' + CX + '" cy="' + CY + '" r="17" fill="url(#' + g + '-sun)"/>' +
        '<g class="sls-front"></g>' +
        '</svg>';

      var svg = el.firstChild;
      var back = svg.querySelector('.sls-back'), front = svg.querySelector('.sls-front');
      var corona = svg.querySelector('.sls-corona');

      var planets = PLANETS.map(function (p, i) {
        var grp = document.createElementNS(NS, 'g');
        var r = p.r, html = '';
        if (p.ring) html += '<path d="M' + (-r * 2.2) + ' 0A' + (r * 2.2) + ' ' + (r * 0.62) + ' 0 0 1 ' + (r * 2.2) + ' 0" fill="none" stroke="#fde68a" stroke-opacity=".75" stroke-width="1.8" transform="rotate(-14)"/>';
        html += '<circle r="' + r + '" fill="url(#' + g + '-' + p.name + ')"/>';
        if (p.bands) html += '<path d="M' + (-r * 0.92) + ' -2.6H' + (r * 0.92) + 'M' + (-r * 0.97) + ' 1.4H' + (r * 0.97) + 'M' + (-r * 0.75) + ' 4.8H' + (r * 0.75) + '" stroke="#7c2d12" stroke-opacity=".35" stroke-width="1.3"/>';
        html += '<circle class="sls-night" r="' + r + '" fill="url(#' + g + '-night)"/>';
        if (p.ring) html += '<path d="M' + (-r * 2.2) + ' 0A' + (r * 2.2) + ' ' + (r * 0.62) + ' 0 0 0 ' + (r * 2.2) + ' 0" fill="none" stroke="#fde68a" stroke-opacity=".9" stroke-width="1.8" transform="rotate(-14)"/>';
        if (p.moon) html += '<circle class="sls-moon" r="1.3" fill="#e5e7eb"/>';
        grp.innerHTML = html;
        front.appendChild(grp);
        return {
          el: grp, a: p.a, night: grp.querySelector('.sls-night'), moon: grp.querySelector('.sls-moon'),
          w: Math.PI * 2 / (YEAR * Math.pow(p.a / 34, 1.5)),
          ang: (i * 2.399) % (Math.PI * 2), inFront: true
        };
      });

      function frame(dt, t) {
        planets.forEach(function (p) {
          p.ang += p.w * dt;
          var c = Math.cos(p.ang), s = Math.sin(p.ang);
          var x = CX + c * p.a, y = CY + s * p.a * TILT;
          // Far side (above the sun's centre) goes behind the sun.
          var nowFront = s > 0;
          if (nowFront !== p.inFront) { (nowFront ? front : back).appendChild(p.el); p.inFront = nowFront; }
          var k = 0.86 + 0.14 * (s + 1) / 2;
          p.el.setAttribute('transform', 'translate(' + x.toFixed(2) + ' ' + y.toFixed(2) + ') scale(' + k.toFixed(3) + ')');
          var away = Math.atan2(y - CY, x - CX) * 180 / Math.PI;
          p.night.setAttribute('transform', 'rotate(' + away.toFixed(1) + ')');
          if (p.moon) {
            var m = t * 0.004;
            p.moon.setAttribute('cx', (Math.cos(m) * 8).toFixed(2));
            p.moon.setAttribute('cy', (Math.sin(m) * 8 * 0.4).toFixed(2));
          }
        });
        corona.setAttribute('r', (54 + Math.sin(t * 0.0015) * 3).toFixed(2));
      }

      if (reduced()) { frame(0, 0); return; }

      var t = 0, last = 0;
      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          var dt = (last ? Math.min(50, now - last) : 16) * speed;
          last = now;
          t += dt;
          frame(dt, t);
        }
      });
      frame(0, 0);
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);

      // Stars twinkle gently.
      var starEls = svg.querySelectorAll('.sls-star');
      if (starEls.length) {
        var tw = anime.animate(starEls, {
          opacity: function () { return anime.utils.random(0.1, 0.9, 2); },
          duration: function () { return anime.utils.random(1200, 3200); },
          delay: function () { return anime.utils.random(0, 2000); },
          ease: 'inOutSine',
          loop: true,
          alternate: true,
          autoplay: false
        });
        new IntersectionObserver(function (entries) {
          if (entries[0].isIntersecting) tw.play(); else tw.pause();
        }).observe(el);
      }
    }

    function init() {
      document.querySelectorAll('[data-solar-system]').forEach(function (el) {
        if (el._sls) return;   // idempotent
        el._sls = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-solar-system></div>

<!-- slower, no orbit lines: -->
<div data-solar-system data-speed="0.4" data-orbits="false"></div>