Pure CSS Shapes & solids #loop #shape

Spiral staircase in pure CSS

Every tread is the same flat slab pivoting on the pole. One index turns it 30° further and lifts it one step higher: that is the whole spiral.

  • 84 lines of CSS
  • 20 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. Every tread is the same flat slab: a rectangle laid down with rotateX(90deg).
  2. Every length is a multiple of one base unit, --u, so the staircase is the same share of a gallery card, the editor and a recording canvas. The numbers below are those units — the strips even carry theirs as plain numbers in --x, --y and --l, and the CSS multiplies each by --u.
  3. The trick is transform-origin: calc(-5 * var(--u)) 50%, a point five units to the left of the slab, which is where the pole's axis is. Every rotation now pivots around the pole.
  4. One index does the rest: rotateY(i × 30deg) turns the tread around the pole, translateY(calc(i × -8 × var(--u))) lifts it one step. Turn + lift = spiral.
  5. The sides are ten thin strips round the tread's edge, each folded straight down by one step: the two long sides, both ends, and three short facets on each rounded corner, so the side follows the curve. The pole is two crossed planes, which read as a round post from any angle.

Key techniques

  • transform-origin on the pole axis
  • translateY + rotateY from one --i
  • sides as strips round the edge, corners as facets
  • crossed planes as a round pole

Copy-paste code

HTML 20 lines

<div class="scene">
  <div class="stairs">
    <b></b>
    <b></b>
    <i style="--i:0"><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s></i>
    <i style="--i:1"><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s></i>
    <i style="--i:2"><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s></i>
    <i style="--i:3"><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s></i>
    <i style="--i:4"><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s></i>
    <i style="--i:5"><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s></i>
    <i style="--i:6"><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s></i>
    <i style="--i:7"><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s></i>
    <i style="--i:8"><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s></i>
    <i style="--i:9"><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s></i>
    <i style="--i:10"><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s></i>
    <i style="--i:11"><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s></i>
    <i style="--i:12"><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s></i>
    <i style="--i:13"><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s><s></s></i>
  </div>
</div>

CSS 84 lines

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

.stairs {
  position: relative;
  width: calc(130 * var(--u));
  height: calc(136 * var(--u));
  transform-style: preserve-3d;
  animation: spin 16s linear infinite;
}

.stairs i {
  --hue: calc(253 - var(--i) * 6); /* violet at the bottom, teal at the top */
  position: absolute;
  left: calc(50% + 5 * var(--u)); /* start just outside the pole */
  top: calc(98 * var(--u));
  width: calc(58 * var(--u));
  height: calc(28 * var(--u));
  /* an inner line plus a soft shade instead of a hard border: treads near eye level are seen
     almost edge-on, where a hairline breaks up; the shade survives. The glow is here, on the 14
     treads, not on the 140 side strips: a blur on every strip cost four screen refreshes a frame */
  box-shadow:
    inset 0 0 0 calc(1 * var(--u)) hsl(var(--hue) 90% 82%),
    inset 0 0 calc(8 * var(--u)) hsl(var(--hue) 90% 82% / 0.25),
    0 0 calc(10 * var(--u)) hsl(var(--hue) 85% 64% / 0.6);
  border-radius: 0 calc(6 * var(--u)) calc(6 * var(--u)) 0;
  background: hsl(var(--hue) 85% 64% / 0.62);
  transform-origin: calc(-5 * var(--u)) 50%; /* on the pole's axis */
  transform-style: preserve-3d;
  transform:
    translateY(calc(var(--i) * -8 * var(--u)))  /* one step up ... */
    rotateY(calc(var(--i) * 30deg))             /* ... and 30deg further round */
    rotateX(90deg);                             /* lie flat */
}

/* The sides go all the way round the tread: both long edges, the outer end and the two rounded
   corners (three short facets each), so the side follows the rounding. Each strip starts at a
   point on the edge (--x, --y), turns to run along it (--a), then folds straight down one step. */
.stairs s {
  position: absolute;
  left: calc(var(--x) * var(--u));
  top: calc(var(--y) * var(--u));
  width: calc((var(--l) + 0.5) * var(--u)); /* a hair longer, so neighbours overlap: no seams */
  height: calc(8 * var(--u));
  background: hsl(var(--hue) 45% 30% / 0.82); /* less see-through than the top */
  transform-origin: 0 0;
  transform: rotate(calc(var(--a) * 1deg)) rotateX(-90deg);
}

.stairs s:nth-child(1) { --x: 0; --y: 0; --l: 52; --a: 0; }
.stairs s:nth-child(2) { --x: 0; --y: 28; --l: 52; --a: 0; }
.stairs s:nth-child(3) { --x: 58; --y: 6; --l: 16; --a: 90; }
.stairs s:nth-child(4) { --x: 52; --y: 0; --l: 3.11; --a: 15; }
.stairs s:nth-child(5) { --x: 55; --y: 0.8; --l: 3.11; --a: 45; }
.stairs s:nth-child(6) { --x: 57.2; --y: 3; --l: 3.11; --a: 75; }
.stairs s:nth-child(7) { --x: 58; --y: 22; --l: 3.11; --a: 105; }
.stairs s:nth-child(8) { --x: 57.2; --y: 25; --l: 3.11; --a: 135; }
.stairs s:nth-child(9) { --x: 55; --y: 27.2; --l: 3.11; --a: 165; }
.stairs s:nth-child(10) { --x: 0; --y: 0; --l: 28; --a: 90; } /* the inner end, at the pole */

/* pole: two crossed planes */
.stairs b {
  position: absolute;
  left: calc(50% - 4 * var(--u));
  top: 0;
  width: calc(8 * var(--u));
  height: 100%;
  border-radius: calc(4 * var(--u));
  background: linear-gradient(90deg, #3a3f63, #eceefb, #3a3f63);
  opacity: 0.85;
}

.stairs b + b {
  transform: rotateY(90deg);
}

@keyframes spin {
  from { transform: rotateX(-20deg) rotateY(0deg); }
  to   { transform: rotateX(-20deg) rotateY(360deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.