Solar system
Pure CSSOrbits are rings on a tilted plane, each spinning at its own speed. Every planet undoes both rotations, so it always faces the camera.
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.
Your edited version
rotate(a), a translateY of −68 units, then rotate(-a): turn, walk out to the rim, turn back. It lands on the rim, upright.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.transform-origin is the pivot. The body is a pseudo-element hanging below it, free to swing a few degrees on its own.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.--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.rotate → translate → rotate backcounter-rotating keyframestransform-origin at the pivotspoke angles and car slots in --a / --i<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>
.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); }
}