Pure CSS Shapes & solids #loop #shape

Ring torus in pure CSS

A donut made of 24 circles. Each one turns to its angle and walks out with translateX, so it stands edge-on around the hole like a slice of the tube.

  • 36 lines of CSS
  • 28 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. A torus is a circle swept around an axis, so build it from its cross-sections: 24 identical circles (border-radius: 50%).
  2. Each ring gets rotateY(i × 15deg) and then translateX, not translateZ. That keeps it in the plane through the axis, edge-on to the path, like a slice of the tube. (translateZ would lay it tangent, like a carousel panel.)
  3. The translateX distance is the radius from the hole's centre to the tube's centre; the ring size is the tube's thickness. Both are multiples of one base unit, --u, so the torus is the same share of a gallery card, the editor and a recording canvas.
  4. Colour follows cos(i × 15deg): teal at ring 0, violet halfway round, teal again at the end, so there is no seam.

Key techniques

  • rotateY + translateX (not translateZ)
  • border-radius: 50% cross-sections
  • colour from cos(i × 15deg): no seam
  • two-axis spin

Copy-paste code

HTML 28 lines

<div class="scene">
  <div class="torus">
    <i style="--i:0"></i>
    <i style="--i:1"></i>
    <i style="--i:2"></i>
    <i style="--i:3"></i>
    <i style="--i:4"></i>
    <i style="--i:5"></i>
    <i style="--i:6"></i>
    <i style="--i:7"></i>
    <i style="--i:8"></i>
    <i style="--i:9"></i>
    <i style="--i:10"></i>
    <i style="--i:11"></i>
    <i style="--i:12"></i>
    <i style="--i:13"></i>
    <i style="--i:14"></i>
    <i style="--i:15"></i>
    <i style="--i:16"></i>
    <i style="--i:17"></i>
    <i style="--i:18"></i>
    <i style="--i:19"></i>
    <i style="--i:20"></i>
    <i style="--i:21"></i>
    <i style="--i:22"></i>
    <i style="--i:23"></i>
  </div>
</div>

CSS 36 lines

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

.torus {
  position: relative;
  width: calc(40 * var(--u));  /* tube thickness */
  height: calc(40 * var(--u));
  transform-style: preserve-3d;
  animation: tumble 16s linear infinite;
}

.torus i {
  --hue: calc(214 - 39 * cos(var(--i) * 15deg)); /* 175 teal ... 253 violet ... 175 */
  position: absolute;
  inset: 0;
  border: calc(2 * var(--u)) solid hsl(var(--hue) 85% 64%);
  border-radius: 50%;
  background: hsl(var(--hue) 85% 64% / 0.14);
  box-shadow: inset 0 0 calc(10 * var(--u)) hsl(var(--hue) 85% 64% / 0.45);
  /* 24 × 15deg = 360deg; translateX keeps each ring edge-on to the circle */
  transform: rotateY(calc(var(--i) * 15deg)) translateX(calc(56 * var(--u)));
}

/* Every ring stands in a plane through the axis, so looked at straight down the axis they are
   all edge-on at once and the torus draws nothing. So it never tumbles through that view: it is
   held 50deg off it (rotateX), spins on its own axis (the inner rotateY) and swings round the
   vertical (the outer rotateY), which keeps the axis at least 40deg from the line of sight.
   Both spins end 360deg after they start, so the loop is seamless. */
@keyframes tumble {
  from { transform: rotateZ(16deg) rotateY(0deg) rotateX(-50deg) rotateY(0deg); }
  to   { transform: rotateZ(16deg) rotateY(360deg) rotateX(-50deg) rotateY(360deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.