Pure CSS Shapes & solids #loop #sass-loop

Atom orbits in pure CSS

Three rings tilted into different planes, each spinning inside its own plane.

  • 54 lines of CSS
  • 8 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

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, the inner ring only animates rotateZ.
  3. The electron is just a dot pinned to the top of the ring — the ring’s spin carries it around.
  4. Different tilts + different durations = an atom.

Key techniques

  • preserve-3d
  • static tilt wrapper + spinning child
  • Sass @each

Copy-paste code

HTML 8 lines

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

CSS 54 lines

.scene {
  perspective: 800px;
}

.atom {
  position: relative;
  width: 200px;
  height: 200px;
  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 24px #8b6cff;
}

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

.plane:nth-of-type(1) { transform: rotateX(72deg);                 color: #2ee6d6; }
.plane:nth-of-type(2) { transform: rotateZ(60deg)  rotateX(72deg); color: #ff4d9d; }
.plane:nth-of-type(3) { transform: rotateZ(-60deg) rotateX(72deg); color: #ffb547; }

.orbit {
  position: absolute;
  inset: 0;
  border: 2px solid currentColor;
  border-radius: 50%;
  animation: orbit-spin 3s linear infinite;
}

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

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

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

Sass source 67 lines

@use 'sass:list';

$planes: (
  1: (rotateX(72deg), var(--accent-2), 2.4s),
  2: (rotateZ(60deg) rotateX(72deg), var(--hot), 3.1s),
  3: (rotateZ(-60deg) rotateX(72deg), var(--warm), 3.8s),
);

.d-orbit {
  position: relative;
  width: 170px;
  height: 170px;
  transform-style: preserve-3d;

  // nucleus
  b {
    position: absolute;
    inset: 40%;
    border-radius: 50%;
    background: radial-gradient(circle at 35% 35%, #fff, var(--accent) 60%);
    box-shadow: 0 0 24px var(--accent);
  }

  // The plane holds the static tilt…
  &__plane {
    position: absolute;
    inset: 0;
    transform-style: preserve-3d;
  }

  // …and the ring spins inside it, so the two transforms never fight.
  &__ring {
    position: absolute;
    inset: 0;
    border: 2px solid currentColor;
    border-radius: 50%;
    animation: d-orbit-spin 3s linear infinite;

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

  @each $n, $plane in $planes {
    &__plane:nth-of-type(#{$n}) {
      transform: #{list.nth($plane, 1)};
      color: list.nth($plane, 2);

      .d-orbit__ring {
        animation-duration: list.nth($plane, 3);
      }
    }
  }
}

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