Pure CSS Shapes & solids #loop #shape #space

Ringed planet in pure CSS

Eleven latitude discs stack into a banded sphere around a shaded disc that always faces you. That disc cuts the rings in half, so their far side slips behind the planet while a moon circles through.

  • 115 lines of CSS
  • 24 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. The sphere is 11 flat discs, one every 15° of latitude: a disc at latitude a sits R·sin(a) above the equator and has radius R·cos(a). CSS does the trig itself: cos(var(--k) * 15deg).
  2. Seen from above, each disc's rim peeks out below the one above it, and those rims are the planet's bands.
  3. A shaded disc through the centre always faces you: it undoes every turn above it, innermost first. It hides exactly what is behind the planet and nothing in front of it, just like a real ball would.
  4. The rings lie flat in the equator plane and cut through that disc. The browser splits intersecting planes and sorts the pieces, so the far half of each ring slips behind the planet and the near half passes in front.
  5. The moon rides a slightly inclined orbit circle; two nested counter-turns (one on the orbit's beat, one on the spin's) keep it facing you.
  6. Every length is a multiple of one base unit, --u, tied to the canvas, so the planet, its rings and its moon are the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • latitude discs: R·cos / R·sin
  • billboard disc hides what is behind
  • intersecting planes are split and sorted
  • nested counter-rotations

Copy-paste code

HTML 24 lines

<div class="scene">
  <div class="planet">
    <div class="body">
      <i style="--k:-5"></i>
      <i style="--k:-4"></i>
      <i style="--k:-3"></i>
      <i style="--k:-2"></i>
      <i style="--k:-1"></i>
      <i style="--k:0"></i>
      <i style="--k:1"></i>
      <i style="--k:2"></i>
      <i style="--k:3"></i>
      <i style="--k:4"></i>
      <i style="--k:5"></i>
      <u></u>
      <b></b>
      <b></b>
      <b></b>
      <div class="orbit">
        <div class="arm"><div class="moon"></div></div>
      </div>
    </div>
  </div>
</div>

CSS 115 lines

.scene {
  /* one base unit: every length below is a multiple of it, so the planet is the same share of a
     card, the editor, a full screen and a recording canvas */
  --u: 0.46vmin;
  perspective: calc(800 * var(--u));
}

.planet {
  --R: calc(40 * var(--u));
  position: relative;
  width: 0;
  height: 0;
  transform-style: preserve-3d;
  animation: spin 40s linear infinite;
}

/* the planet's frame, tilted 12deg on its axis */
.body {
  position: absolute;
  transform-style: preserve-3d;
  transform: rotateZ(-12deg);
}

/* latitude discs: k = -5 ... 5, every 15deg */
.body i {
  --r: calc(cos(var(--k) * 15deg) * var(--R));
  --c: color-mix(in srgb, #ffb547 80%, #ff4d9d);
  position: absolute;
  top: calc(var(--r) * -1);
  left: calc(var(--r) * -1);
  width: calc(var(--r) * 2);
  height: calc(var(--r) * 2);
  border-radius: 50%;
  background: radial-gradient(circle, var(--c) 55%, color-mix(in srgb, var(--c) 70%, #000));
  transform: translateY(calc(sin(var(--k) * 15deg) * var(--R) * -1)) rotateX(90deg);
}

.body i:nth-child(odd)    { --c: color-mix(in srgb, #ffb547 55%, #fff); }
.body i:nth-child(4n + 1) { --c: color-mix(in srgb, #ff4d9d 70%, #ffb547); }
.body i:nth-child(-n + 2),
.body i:nth-child(n + 10) { --c: color-mix(in srgb, #8b6cff 55%, #ffb547); } /* polar caps */

/* the body: a shaded disc that always faces you (undoes the tilt, the spin, the camera height) */
.body u {
  position: absolute;
  inset: calc(var(--R) * -1);
  border-radius: 50%;
  background: radial-gradient(circle at 36% 30%, #ffdaa3, #ffb547 32%, #ff6c83 62%, #614cb3);
  animation: face-you 40s linear infinite;
}

/* rings: flat circles in the equator plane */
.body b {
  position: absolute;
  border-radius: 50%;
  transform: rotateX(90deg);
}

.body b:nth-of-type(1) { inset: calc(-52 * var(--u));   border: calc(7 * var(--u)) solid rgb(139 108 255 / 0.38); }
.body b:nth-of-type(2) { inset: calc(-62 * var(--u));   border: calc(8 * var(--u)) solid rgb(197 186 253 / 0.85); }
.body b:nth-of-type(3) { inset: calc(-72.8 * var(--u)); border: calc(2 * var(--u)) solid rgb(139 108 255 / 0.85); }

/* the moon's orbit: a faint dashed circle, inclined 8deg, turning on its own axis */
.orbit {
  position: absolute;
  inset: calc(-92 * var(--u));
  border: calc(1 * var(--u)) dashed rgb(46 230 214 / 0.4);
  border-radius: 50%;
  transform-style: preserve-3d;
  animation: orbit 7s linear infinite;
}

/* undo the orbit's turn ... */
.arm {
  position: absolute;
  top: calc(-7 * var(--u));
  left: calc(50% - 7 * var(--u));
  width: calc(14 * var(--u));
  height: calc(14 * var(--u));
  transform-style: preserve-3d;
  animation: unorbit 7s linear infinite;
}

/* ... then the incline, the tilt, the spin and the camera height */
.moon {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, #fff 0 8%, #2ee6d6 45%, #125c56);
  animation: moon 40s linear infinite;
}

@keyframes spin {
  from { transform: rotateX(-22deg) rotateY(0deg); }
  to   { transform: rotateX(-22deg) rotateY(360deg); }
}

@keyframes face-you {
  from { transform: rotateZ(12deg) rotateY(0deg) rotateX(22deg); }
  to   { transform: rotateZ(12deg) rotateY(-360deg) rotateX(22deg); }
}

@keyframes orbit {
  from { transform: rotateX(82deg) rotateZ(0deg); }
  to   { transform: rotateX(82deg) rotateZ(360deg); }
}

@keyframes unorbit {
  to { transform: rotateZ(-360deg); }
}

@keyframes moon {
  from { transform: rotateX(-82deg) rotateZ(12deg) rotateY(0deg) rotateX(22deg); }
  to   { transform: rotateX(-82deg) rotateZ(12deg) rotateY(-360deg) rotateX(22deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.