Pure CSS Shapes & solids #loop #shape

Cylinder in pure CSS

A glass tube with rings of light rising inside. CSS has no curves in 3D, so the tube is 24 flat strips; Sass computes their width.

  • 75 lines of CSS
  • 30 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

How it works

  1. There are no curved surfaces in CSS 3D, so approximate: 24 flat strips arranged in a circle.
  2. Strip width for a closed ring is 2 × r × tan(180° / n). For r = 80px and n = 24 that is ≈ 21px; add 1px so no seams show.
  3. Placement is the carousel trick: rotateY(i × 15deg) translateZ(r). Semi-transparent strips let the far side show through, which is what makes it read as glass.
  4. The rings of light are circles laid flat with rotateX(90deg). They rise by animating translate, which is applied before transform, so "up" is the tube's own vertical.

Key techniques

  • strip width = 2r × tan(180° / n)
  • rotateY + translateZ
  • per-strip shading

Copy-paste code

HTML 30 lines

<div class="scene">
  <div class="cylinder">
    <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>
    <b></b><s></s>
    <u></u><u></u><u></u>
  </div>
</div>

CSS 75 lines

.scene {
  perspective: 800px;
}

.cylinder {
  --r: 80px;
  --h: 180px;
  position: relative;
  width: 22px;                 /* 2 × 80 × tan(7.5deg) + 1px */
  height: var(--h);
  transform-style: preserve-3d;
  animation: cylinder-spin 14s linear infinite;
}

/* see-through strips: the far side of the tube shows through the near one */
.cylinder i {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(rgb(255 255 255 / 0.55) 0 2px, transparent 2px calc(100% - 2px), rgb(255 255 255 / 0.4) calc(100% - 2px)),
    linear-gradient(hsl(calc(170 + var(--i) * 6) 85% 62% / 0.5), hsl(calc(250 + var(--i) * 3) 80% 58% / 0.14));
  transform: rotateY(calc(var(--i) * 15deg)) translateZ(var(--r));
}

/* lid (b) and floor (s): glass discs laid flat */
.cylinder b,
.cylinder s {
  position: absolute;
  left: calc(50% - var(--r));
  width: calc(var(--r) * 2);
  height: calc(var(--r) * 2);
  box-sizing: border-box;
  border: 1px solid rgb(255 255 255 / 0.4);
  border-radius: 50%;
  background: radial-gradient(circle, rgb(255 255 255 / 0.16), rgb(139 108 255 / 0.22) 70%);
  transform: rotateX(90deg);
}

.cylinder b { top: calc(var(--r) * -1); }

.cylinder s {
  top: calc(var(--h) - var(--r));
  box-shadow: 0 0 40px rgb(46 230 214 / 0.55);
}

/* rings of light rising through the tube. `translate` moves them up the tube's own vertical,
   before the rotateX that lays them flat. */
.cylinder u {
  position: absolute;
  top: calc(var(--h) - var(--r) + 8px);
  left: calc(50% - var(--r) + 8px);
  width: calc(var(--r) * 2 - 16px);
  height: calc(var(--r) * 2 - 16px);
  box-sizing: border-box;
  border: 2px solid #2ee6d6;
  border-radius: 50%;
  box-shadow: 0 0 14px #2ee6d6, inset 0 0 14px #2ee6d6;
  opacity: 0;
  transform: rotateX(90deg);
  animation: rise 3.6s linear infinite;
}

.cylinder u:nth-of-type(2) { animation-delay: -1.2s; }
.cylinder u:nth-of-type(3) { animation-delay: -2.4s; }

@keyframes cylinder-spin {
  from { transform: rotateX(-20deg) rotateY(0deg); }
  to   { transform: rotateX(-20deg) rotateY(360deg); }
}

@keyframes rise {
  0%       { opacity: 0; translate: 0 -4px; }
  15%, 80% { opacity: 1; }
  100%     { opacity: 0; translate: 0 calc(var(--h) * -1 + 12px); }
}

Sass source 103 lines

@use 'sass:math';

$n: 24;
$r: 58px;
$h: 132px;
// Width of one flat strip so that $n of them close the circle (+1px overlap against seams).
$strip: 2 * $r * math.tan(math.div(180deg, $n)) + 1px;

// A glass tube: 24 see-through strips (the far side shows through the near one), glowing rims,
// and rings of light travelling up inside it.
.d-cylinder {
  position: relative;
  width: $strip;
  height: $h;
  transform-style: preserve-3d;
  animation: d-cylinder-spin 14s linear infinite;

  // --i (0…23) comes from the markup: the hue sweeps round the tube
  i {
    position: absolute;
    inset: 0;
    background:
      // bright rims at the top and bottom edge
      linear-gradient(rgb(255 255 255 / 0.55) 0 2px, transparent 2px calc(100% - 2px), rgb(255 255 255 / 0.4) calc(100% - 2px)),
      linear-gradient(hsl(calc(170 + var(--i) * 6) 85% 62% / 0.5), hsl(calc(250 + var(--i) * 3) 80% 58% / 0.14));
    transform: rotateY(calc(var(--i) * #{math.div(360deg, $n)})) translateZ($r);
  }

  // lid and floor: glass discs with a rim
  b,
  s {
    position: absolute;
    left: calc(50% - #{$r});
    width: $r * 2;
    height: $r * 2;
    border: 1px solid rgb(255 255 255 / 0.4);
    border-radius: 50%;
    background: radial-gradient(circle, rgb(255 255 255 / 0.16), color-mix(in srgb, var(--accent) 22%, transparent) 70%);
    transform: rotateX(90deg);
  }

  b {
    top: -$r;
  }

  s {
    top: $h - $r;
    box-shadow: 0 0 40px color-mix(in srgb, var(--accent-2) 55%, transparent);
  }

  // three rings of light rising through the tube, one after another; `translate` moves them in
  // the tube's own vertical, before the rotateX that lays them flat
  u {
    position: absolute;
    top: $h - $r;
    left: calc(50% - #{$r - 6px});
    width: ($r - 6px) * 2;
    height: ($r - 6px) * 2;
    border: 2px solid var(--accent-2);
    border-radius: 50%;
    box-shadow:
      0 0 14px var(--accent-2),
      inset 0 0 14px var(--accent-2);
    opacity: 0;
    transform: rotateX(90deg);
    animation: d-cylinder-rise 3.6s linear infinite;

    &:nth-of-type(2) {
      animation-delay: -1.2s;
    }

    &:nth-of-type(3) {
      animation-delay: -2.4s;
    }
  }
}

@keyframes d-cylinder-spin {
  from {
    transform: rotateX(-20deg) rotateY(0deg);
  }

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

@keyframes d-cylinder-rise {
  0% {
    opacity: 0;
    translate: 0 -4px;
  }

  15%,
  80% {
    opacity: 1;
  }

  100% {
    opacity: 0;
    translate: 0 ($h * -1 + 12px);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.