Pure CSS Shapes & solids #loop

Atom orbits in pure CSS

Three rings tilted into different planes, each with an electron going round inside its own plane.

  • 66 lines of CSS
  • 8 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. An animation overwrites the whole transform, so a ring cannot both hold a tilt and animate a spin.
  2. Fix: two elements. The outer plane holds the static tilt, and an inner arm only animates rotateZ.
  3. The electron is a dot pinned to the top of the arm — the arm’s spin carries it around, and the arm draws nothing at all.
  4. Different tilts + different durations = an atom.
  5. A round thing still lives in a square box, and that box is what has to fit the canvas. Spin the ring itself and its corners sweep a circle √2 the ring’s width; spin an empty arm instead and the ring’s box never moves. Same picture, 40% more of it on the screen.
  6. For the same reason a plane is tilted with rotateY then rotateX, never with rotateZ. A circle only cares which way its plane faces, and those two reach every direction, while rotateZ turns the square in the screen plane: 60° of it costs 37% more room for the same picture.
  7. Every length is a multiple of one base unit, --u, so the atom is the same share of a gallery card, the editor and a recording canvas. The rings are 200 of those units across.

Key techniques

  • preserve-3d
  • static tilt wrapper + spinning child
  • own tilt and duration per ring

Copy-paste code

HTML 8 lines

<div class="scene">
  <div class="atom">
    <b></b>
    <div class="plane"><u></u><div class="arm"><i></i></div></div>
    <div class="plane"><u></u><div class="arm"><i></i></div></div>
    <div class="plane"><u></u><div class="arm"><i></i></div></div>
  </div>
</div>

CSS 66 lines

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

.atom {
  position: relative;
  width: calc(200 * var(--u));
  height: calc(200 * var(--u));
  transform-style: preserve-3d;
}

.atom b {
  position: absolute;
  inset: 40%;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, #fff, #8b6cff 60%);
  box-shadow: 0 0 calc(24 * var(--u)) #8b6cff;
}

.plane {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}

/* the same three planes as rotateZ(0 / ±60deg) rotateX(72deg), said with rotateY instead: each
   ring faces exactly the same way, but no square is turned in the screen plane */
.plane:nth-of-type(1) { transform: rotateX(72deg);                       color: #2ee6d6; }
.plane:nth-of-type(2) { transform: rotateY(69.44deg)  rotateX(28.39deg); color: #ff4d9d; }
.plane:nth-of-type(3) { transform: rotateY(-69.44deg) rotateX(28.39deg); color: #ffb547; }

/* the ring: drawn, and never turned — so its square box is only ever as wide as the circle in it */
.plane u {
  position: absolute;
  inset: 0;
  border: calc(2 * var(--u)) solid currentColor;
  border-radius: 50%;
}

/* the arm: turned, and draws nothing — it exists only to carry the electron round */
.arm {
  position: absolute;
  inset: 0;
  animation: orbit-spin 3s linear infinite;
}

.plane:nth-of-type(2) .arm { animation-duration: 3.7s; }
.plane:nth-of-type(3) .arm { animation-duration: 4.4s; }

.arm i {
  position: absolute;
  top: calc(-7 * var(--u));
  left: calc(50% - 6 * var(--u));
  width: calc(12 * var(--u));
  height: calc(12 * var(--u));
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 calc(12 * var(--u)) currentColor;
}

@keyframes orbit-spin {
  to { transform: rotateZ(360deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.