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.

  • 112 lines of CSS
  • 24 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

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.

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 112 lines

.scene {
  perspective: 800px;
}

.planet {
  --R: 40px;
  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: -52px;   border: 7px solid rgb(139 108 255 / 0.38); }
.body b:nth-of-type(2) { inset: -62px;   border: 8px solid rgb(197 186 253 / 0.85); }
.body b:nth-of-type(3) { inset: -72.8px; border: 2px 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: -92px;
  border: 1px 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: -7px;
  left: calc(50% - 7px);
  width: 14px;
  height: 14px;
  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); }
}

Sass source 179 lines

@use 'sass:math';

$R: 40px; // planet radius
$view: 22deg; // camera height above the horizon
$axis: 12deg; // the planet's axial tilt
$spin: 40s;
$moon-t: 7s;
$orbit: 92px;
$incline: 8deg; // the moon's orbit against the rings

// Undo every turn above an element, innermost first, so it faces you (billboarding).
$face-body: rotateZ($axis);
$face-view: rotateX($view);

.d-planet {
  position: relative;
  width: 0;
  height: 0;
  transform-style: preserve-3d;
  animation: d-planet-spin $spin linear infinite;

  // the planet's own frame: tilted on its axis. Discs, rings and orbit all live in it.
  &__body {
    position: absolute;
    transform-style: preserve-3d;
    transform: rotateZ(-$axis);
  }

  // 11 latitude discs, every 15deg from -75deg to 75deg: a disc at latitude a sits R·sin(a) up and
  // has radius R·cos(a). Together their rims trace the sphere; their tops show as bands.
  i {
    --r: calc(cos(var(--k) * 15deg) * #{$R});
    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) * #{-$R})) rotateX(90deg);

    --c: color-mix(in srgb, var(--warm) 80%, var(--hot));

    &:nth-child(odd) {
      --c: color-mix(in srgb, var(--warm) 55%, #fff);
    }

    &:nth-child(4n + 1) {
      --c: color-mix(in srgb, var(--hot) 70%, var(--warm));
    }

    // polar caps a little cooler
    &:nth-child(-n + 2),
    &:nth-child(n + 10) {
      --c: color-mix(in srgb, var(--accent) 55%, var(--warm));
    }
  }

  // The body: a shaded disc through the centre that always faces you. It hides whatever passes
  // behind the planet and nothing in front of it, exactly as a real sphere would.
  u {
    position: absolute;
    inset: -$R;
    border-radius: 50%;
    background: radial-gradient(
      circle at 36% 30%,
      color-mix(in srgb, var(--warm) 50%, #fff),
      var(--warm) 32%,
      color-mix(in srgb, var(--hot) 70%, var(--warm)) 62%,
      color-mix(in srgb, var(--accent) 70%, #000) 100%
    );
    animation: d-planet-face $spin linear infinite;
  }

  // rings: flat circles in the equator plane (rotateX(90deg)). The body disc crosses them, so
  // the browser draws their far half behind the planet and the near half in front.
  b {
    position: absolute;
    border-radius: 50%;
    transform: rotateX(90deg);

    &:nth-of-type(1) {
      inset: -$R * 1.3;
      border: 7px solid color-mix(in srgb, var(--accent) 38%, transparent);
    }

    &:nth-of-type(2) {
      inset: -$R * 1.55;
      border: 8px solid color-mix(in srgb, color-mix(in srgb, var(--accent) 40%, var(--text)) 85%, transparent);
    }

    &:nth-of-type(3) {
      inset: -$R * 1.82;
      border: 2px solid color-mix(in srgb, var(--accent) 85%, transparent);
    }
  }

  // the moon's orbit: a faint dashed circle, a little inclined, turning on its own axis (rotateZ)
  &__orbit {
    position: absolute;
    inset: -$orbit;
    border: 1px dashed color-mix(in srgb, var(--accent-2) 40%, transparent);
    border-radius: 50%;
    transform-style: preserve-3d;
    animation: d-planet-orbit $moon-t linear infinite;
  }

  // the moon rides the top of the orbit circle; two nested turns undo the orbit and then the
  // tilts and the spin, so it is always a round face
  &__arm {
    position: absolute;
    top: -7px;
    left: calc(50% - 7px);
    width: 14px;
    height: 14px;
    transform-style: preserve-3d;
    animation: d-planet-unorbit $moon-t linear infinite;
  }

  &__moon {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: radial-gradient(
      circle at 34% 30%,
      #fff 0 8%,
      var(--accent-2) 45%,
      color-mix(in srgb, var(--accent-2) 40%, #000) 100%
    );
    animation: d-planet-moon $spin linear infinite;
  }
}

@keyframes d-planet-spin {
  from {
    transform: rotateX(-$view) rotateY(0deg);
  }

  to {
    transform: rotateX(-$view) rotateY(360deg);
  }
}

// inverse of body tilt, then of the spin, then of the camera height
@keyframes d-planet-face {
  from {
    transform: $face-body rotateY(0deg) $face-view;
  }

  to {
    transform: $face-body rotateY(-360deg) $face-view;
  }
}

@keyframes d-planet-orbit {
  from {
    transform: rotateX(90deg - $incline) rotateZ(0deg);
  }

  to {
    transform: rotateX(90deg - $incline) rotateZ(360deg);
  }
}

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

@keyframes d-planet-moon {
  from {
    transform: rotateX($incline - 90deg) $face-body rotateY(0deg) $face-view;
  }

  to {
    transform: rotateX($incline - 90deg) $face-body rotateY(-360deg) $face-view;
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.