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

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.

  • 63 lines of CSS
  • 20 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed
GitHub

How it works

  1. Every tread is the same flat slab: a rectangle laid down with rotateX(90deg).
  2. The trick is transform-origin: -5px 50%, a point 5px to the left of the slab, which is where the pole's axis is. Every rotation now pivots around the pole.
  3. One index does the rest: rotateY(i × 30deg) turns the tread around the pole, translateY(i × -8px) lifts it one step. Turn + lift = spiral.
  4. The riser is a ::before hinged on the tread's edge and folded straight down. 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
  • riser as a folded ::before
  • 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"></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>
  </div>
</div>

CSS 63 lines

.scene {
  perspective: 800px;
}

.stairs {
  position: relative;
  width: 130px;
  height: 136px;
  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% + 5px); /* start just outside the pole */
  top: 98px;
  width: 58px;
  height: 28px;
  border: 1px solid hsl(var(--hue) 90% 82%);
  border-radius: 0 6px 6px 0;
  background: hsl(var(--hue) 85% 64% / 0.62);
  transform-origin: -5px 50%; /* on the pole's axis */
  transform-style: preserve-3d;
  transform:
    translateY(calc(var(--i) * -8px))   /* one step up ... */
    rotateY(calc(var(--i) * 30deg))     /* ... and 30deg further round */
    rotateX(90deg);                     /* lie flat */
}

/* riser: hinged on the tread's edge, folded straight down by one step */
.stairs i::before {
  content: '';
  position: absolute;
  left: 0;
  top: 100%;
  width: 100%;
  height: 8px;
  background: hsl(var(--hue) 45% 28%);
  transform-origin: top;
  transform: rotateX(-90deg);
}

/* pole: two crossed planes */
.stairs b {
  position: absolute;
  left: calc(50% - 4px);
  top: 0;
  width: 8px;
  height: 100%;
  border-radius: 4px;
  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); }
}

Sass source 80 lines

@use 'sass:math';

$n: 14;
$turn: 30deg; // each step is turned this much more than the one below
$rise: 8px; // and lifted this much
$len: 58px; // tread length, from the pole outwards
$depth: 28px;
$gap: 5px; // treads start just outside the pole so the planes never intersect
$height: 136px;

.d-stairs {
  position: relative;
  width: 130px;
  height: $height;
  transform-style: preserve-3d;
  animation: d-stairs-spin 16s linear infinite;

  // A tread is a rectangle laid flat (rotateX 90deg) whose transform-origin sits on the pole's
  // axis. One index drives both the turn and the lift: that is the whole spiral.
  i {
    --c: color-mix(in srgb, var(--accent-2) var(--mix), var(--accent));
    position: absolute;
    left: calc(50% + #{$gap});
    top: $height - $depth - 10px;
    width: $len;
    height: $depth;
    border: 1px solid color-mix(in srgb, var(--c) 70%, white);
    border-radius: 0 6px 6px 0;
    background: color-mix(in srgb, var(--c) 62%, transparent);
    transform-origin: -$gap 50%;
    transform-style: preserve-3d;
    transform: translateY(calc(var(--i) * #{-$rise})) rotateY(calc(var(--i) * #{$turn})) rotateX(90deg);

    // riser: a strip hinged on the tread's near edge, folded straight down
    &::before {
      content: '';
      position: absolute;
      left: 0;
      top: 100%;
      width: 100%;
      height: $rise;
      background: color-mix(in srgb, var(--c) 40%, var(--bg));
      transform-origin: top;
      transform: rotateX(-90deg);
    }
  }

  @for $i from 0 to $n {
    i:nth-of-type(#{$i + 1}) {
      --i: #{$i};
      --mix: #{math.round(math.div($i, $n - 1) * 100) * 1%};
    }
  }

  // pole: two crossed planes read as a round post from every angle
  b {
    position: absolute;
    left: calc(50% - 4px);
    top: 0;
    width: 8px;
    height: 100%;
    border-radius: 4px;
    background: linear-gradient(90deg, var(--border-strong), var(--text), var(--border-strong));
    opacity: 0.85;

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

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

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