Pure CSS Shapes & solids #loop #shape #sass-loop

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.

  • 29 lines of CSS
  • 28 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

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.
  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
  • Sass @for colour ramp
  • 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 29 lines

.scene {
  perspective: 800px;
}

.torus {
  position: relative;
  width: 40px;  /* tube thickness */
  height: 40px;
  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: 2px solid hsl(var(--hue) 85% 64%);
  border-radius: 50%;
  background: hsl(var(--hue) 85% 64% / 0.14);
  box-shadow: inset 0 0 10px 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(56px);
}

/* spins on two axes; both end 360deg after they start, so the loop is seamless */
@keyframes tumble {
  from { transform: rotateZ(24deg) rotateX(-60deg) rotateY(0deg); }
  to   { transform: rotateZ(24deg) rotateX(300deg) rotateY(360deg); }
}

Sass source 45 lines

@use 'sass:math';

$n: 24;
$R: 56px; // from the centre of the hole to the centre of the tube
$d: 40px; // tube diameter

.d-torus {
  position: relative;
  width: $d;
  height: $d;
  transform-style: preserve-3d;
  animation: d-torus-tumble 16s linear infinite;

  // Each ring is one cross-section of the tube. rotateY picks the direction, translateX walks out
  // along that direction, so the ring stands in the plane that contains the axis, edge-on to the
  // circle it sits on (translateZ would lay it tangent instead, like a carousel panel).
  i {
    --c: color-mix(in srgb, var(--accent-2) var(--mix), var(--accent));
    position: absolute;
    inset: 0;
    border: 2px solid var(--c);
    border-radius: 50%;
    background: color-mix(in srgb, var(--c) 14%, transparent);
    box-shadow: inset 0 0 10px color-mix(in srgb, var(--c) 45%, transparent);
    transform: rotateY(calc(var(--i) * #{math.div(360deg, $n)})) translateX($R);
  }

  // colour runs violet, teal, violet around the ring so there is no seam
  @for $i from 0 to $n {
    i:nth-child(#{$i + 1}) {
      --i: #{$i};
      --mix: #{math.round(math.div(math.abs($i - $n * 0.5), $n * 0.5) * 100) * 1%};
    }
  }
}

@keyframes d-torus-tumble {
  from {
    transform: rotateZ(24deg) rotateX(-60deg) rotateY(0deg);
  }

  to {
    transform: rotateZ(24deg) rotateX(300deg) rotateY(360deg);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.