Pure CSS Scenes & objects #loop #scene #wheel

Ferris wheel in pure CSS

The wheel turns on Z while every cabin turns the other way by the same amount, so the cabins stay upright. Two rims at different depths give it thickness.

  • 162 lines of CSS
  • 21 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. Every cabin is a zero-size point at the wheel’s centre with rotate(a), a translateY of −68 units, then rotate(-a): turn, walk out to the rim, turn back. It lands on the rim, upright.
  2. The wheel spins 0 → 360deg. Each cabin appends rotate(0 → -360deg) with the same duration, so the two cancel at every moment and the cabins keep hanging straight down.
  3. Because the cabin element is a point, transform-origin is the pivot. The body is a pseudo-element hanging below it, free to swing a few degrees on its own.
  4. Depth comes cheap: two rims 7 units in front of and behind the centre (translateZ), every spoke drawn twice with ::before/::after, and two A-frames in front of and behind the wheel. Turn the whole thing with rotateY to see it.
  5. Every length is a multiple of one base unit, --u, tied to the canvas: the ride is 200 × 188 units, so it is the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • rotate → translate → rotate back
  • counter-rotating keyframes
  • transform-origin at the pivot
  • spoke angles and car slots in --a / --i

Copy-paste code

HTML 21 lines

<div class="scene">
  <div class="ferris">
    <div class="base"></div>
    <div class="stand"><s></s><s></s></div>
    <div class="wheel">
      <i style="--a:0deg"></i>
      <i style="--a:45deg"></i>
      <i style="--a:90deg"></i>
      <i style="--a:135deg"></i>
      <b style="--i:0;--c:#2ee6d6"></b>
      <b style="--i:1;--c:#ff4d9d"></b>
      <b style="--i:2;--c:#ffb547"></b>
      <b style="--i:3;--c:#8b6cff"></b>
      <b style="--i:4;--c:#2ee6d6"></b>
      <b style="--i:5;--c:#ff4d9d"></b>
      <b style="--i:6;--c:#ffb547"></b>
      <b style="--i:7;--c:#8b6cff"></b>
      <u></u>
    </div>
  </div>
</div>

CSS 162 lines

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

.ferris {
  position: relative;
  width: calc(200 * var(--u));
  height: calc(188 * var(--u));
  transform-style: preserve-3d;
  transform: rotateX(-8deg) rotateY(-28deg);
}

/* a glowing pad, laid flat on the ground */
.base {
  position: absolute;
  left: calc(20 * var(--u));
  top: calc(150 * var(--u));
  width: calc(160 * var(--u));
  height: calc(56 * var(--u));
  border-radius: 50%;
  background: radial-gradient(closest-side, rgb(139 108 255 / 0.4), transparent);
  transform: rotateX(90deg);
}

.stand {
  position: absolute;
  left: calc(38 * var(--u));
  top: calc(74 * var(--u));
  width: calc(124 * var(--u));
  height: calc(104 * var(--u));
  transform-style: preserve-3d;
}

/* an A-frame: a thin inverted V in one polygon */
.stand s {
  position: absolute;
  inset: 0;
  background: linear-gradient(#a18cff, rgb(139 108 255 / 0.4));
  clip-path: polygon(0 100%, 48% 0, 52% 0, 100% 100%, 95.5% 100%, 50% 5%, 4.5% 100%);
  transform: translateZ(calc(-14 * var(--u)));
}

.stand s + s {
  transform: translateZ(calc(14 * var(--u)));
}

.wheel {
  position: absolute;
  left: calc(32 * var(--u));
  top: calc(10 * var(--u));
  width: calc(136 * var(--u));
  height: calc(136 * var(--u));
  transform-style: preserve-3d;
  animation: spin 24s linear infinite;
}

/* two rims at different depths */
.wheel::before,
.wheel::after {
  content: '';
  position: absolute;
  inset: 0;
  border: calc(3 * var(--u)) solid #8b6cff;
  border-radius: 50%;
  box-shadow: 0 0 calc(10 * var(--u)) rgb(139 108 255 / 0.6);
  transform: translateZ(calc(7 * var(--u)));
}

.wheel::after {
  border: calc(3 * var(--u)) dotted #ffb547;
  box-shadow: none;
  transform: translateZ(calc(-7 * var(--u)));
}

/* each spoke is a diameter, drawn once per rim */
.wheel i {
  position: absolute;
  top: 0;
  left: calc(50% - calc(1 * var(--u)));
  width: calc(2 * var(--u));
  height: 100%;
  transform-style: preserve-3d;
  transform: rotate(var(--a));
}

.wheel i::before,
.wheel i::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgb(139 108 255 / 0.6);
  transform: translateZ(calc(7 * var(--u)));
}

.wheel i::after {
  transform: translateZ(calc(-7 * var(--u)));
}

/* a cabin: a point at the centre, pushed out to the rim */
.wheel b {
  --a: calc(var(--i) * 45deg);
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0;
  height: 0;
  animation: cabin 24s linear infinite;
}

/* the body hangs below the point and swings a little */
.wheel b::before {
  content: '';
  position: absolute;
  top: calc(3 * var(--u));
  left: calc(-9 * var(--u));
  width: calc(18 * var(--u));
  height: calc(15 * var(--u));
  border-radius: calc(3 * var(--u)) calc(3 * var(--u)) calc(7 * var(--u)) calc(7 * var(--u));
  background: linear-gradient(var(--c) 0 30%, color-mix(in srgb, var(--c) 30%, #fff) 30% 62%, var(--c) 62%);
  box-shadow: 0 0 calc(8 * var(--u)) var(--c);
  transform-origin: 50% calc(-3 * var(--u));
  animation: swing 1.6s ease-in-out infinite alternate;
  animation-delay: calc(var(--i) * -0.4s);
}

.wheel b::after {
  content: '';
  position: absolute;
  top: calc(-1 * var(--u));
  left: calc(-1 * var(--u));
  width: calc(2 * var(--u));
  height: calc(5 * var(--u));
  background: #ccc;
}

.wheel u {
  position: absolute;
  inset: calc(50% - calc(8 * var(--u)));
  border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, #fff, #ffb547 55%, #ff4d9d);
  transform: translateZ(calc(8 * var(--u)));
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* the last rotate cancels the wheel's rotation */
@keyframes cabin {
  from { transform: rotate(var(--a)) translateY(calc(-68 * var(--u))) rotate(calc(var(--a) * -1)) rotate(0deg); }
  to   { transform: rotate(var(--a)) translateY(calc(-68 * var(--u))) rotate(calc(var(--a) * -1)) rotate(-360deg); }
}

@keyframes swing {
  from { transform: rotate(-5deg); }
  to   { transform: rotate(5deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.