Isometric city
CSS + JSSixteen buildings on a ground plane. Each one is a single element: the roof is lifted by translateZ and two pseudo-element walls hang down from it. The pointer turns the block.
Orbits are rings on a tilted plane, each spinning at its own speed. Every planet undoes both rotations, so it always faces the camera.
Your edited version
rotateX(56deg) and turned a little on screen with rotateZ(-16deg). Every orbit is a ring on that plane.rotateZ(0 → 360deg); its planet sits on the ring’s top edge and rides along. A negative animation-delay starts every planet at a different angle.rotateZ(-angle) rotateX(-56deg) rotateZ(16deg). The planet runs that with the same duration and delay as its ring, so the rotations cancel at every frame. That is billboarding.conic-gradient cut to a thin ring with a mask, on a pseudo-element, so the mask never flattens the planet.clip-path. The far (upper) half is a ::before painted under the ball, an inner <i>, and the near half an ::after painted over it, simply in DOM order. The planet always faces you, so the upper half is the far one at every point of the orbit.--u, tied to the canvas: the widest orbit is 196 units across, and each ring’s radius and planet size are written on it as plain numbers that CSS multiplies by the unit. The plane leans back 56°, not further, so the system stands tall enough in the frame without growing wider than it.rotateX tilted planerotateZ orbit per ringcounter-rotation = billboardingnegative animation-delayring split around the ball (clip-path)<div class="scene">
<div class="system">
<div class="sun"></div>
<div class="orbit" style="--r:34;--t:5s;--s:9;--c:#2ee6d6;--d:-1s"><b></b></div>
<div class="orbit" style="--r:54;--t:9s;--s:13;--c:#ff4d9d;--d:0s"><b></b></div>
<div class="orbit" style="--r:76;--t:14s;--s:11;--c:#8b6cff;--d:-10s"><b></b></div>
<div class="orbit" style="--r:98;--t:22s;--s:16;--c:#ffb547;--d:-3.2s"><b class="ringed"><i></i></b></div>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the system is the same share of a
gallery card, the editor, a full screen and a recording canvas */
--u: 0.42vmin;
display: grid;
place-items: center;
perspective: calc(800 * var(--u));
}
.system,
.system * {
box-sizing: border-box;
}
/* the orbital plane: rolled on screen, then laid back */
.system {
position: relative;
width: calc(200 * var(--u));
height: calc(200 * var(--u));
transform-style: preserve-3d;
transform: rotateZ(-16deg) rotateX(56deg);
}
.sun {
position: absolute;
inset: calc(50% - calc(13 * var(--u)));
border-radius: 50%;
background: radial-gradient(circle at 38% 34%, #fff8e1, #ffb547 45%, #ff4d9d);
box-shadow: 0 0 calc(16 * var(--u)) #ffb547, 0 0 calc(40 * var(--u)) rgb(255 77 157 / 0.45);
transform: rotateX(-56deg) rotateZ(16deg); /* the plane, undone: faces the camera */
}
.orbit {
position: absolute;
inset: calc(50% - var(--r) * var(--u)); /* --r and --s are plain numbers, counted in units */
border: calc(1 * var(--u)) solid color-mix(in srgb, var(--c) 30%, transparent);
border-radius: 50%;
transform-style: preserve-3d;
animation: orbit var(--t) linear var(--d) infinite;
}
/* a fading trail behind the planet: a conic gradient masked down to a thin ring */
.orbit::before {
content: '';
position: absolute;
inset: calc(-2 * var(--u));
border-radius: 50%;
background: conic-gradient(transparent 62%, var(--c));
mask: radial-gradient(closest-side, transparent calc(100% - calc(3 * var(--u))), #000 calc(100% - calc(2 * var(--u))));
}
.orbit b {
position: absolute;
top: calc(var(--s) * var(--u) / -2);
left: calc(50% - var(--s) * var(--u) / 2);
width: calc(var(--s) * var(--u));
height: calc(var(--s) * var(--u));
/* same duration + delay as the ring: the two rotations cancel */
animation: face var(--t) linear var(--d) infinite;
}
.orbit b,
.ringed i {
border-radius: 50%;
background: radial-gradient(circle at 34% 30%, #fff 0 6%, var(--c) 42%, color-mix(in srgb, var(--c) 45%, #000));
}
/* the ring wraps the ball: its far half (::before) is drawn under the ball (the <i>), its near
half (::after) over it, in DOM order. The planet faces the camera, so the far half is always the
upper half of the ellipse: both halves are the same tilted ellipse, centred on the ball and cut
along its long axis, through the ball's centre */
.orbit .ringed {
/* a box as wide as the ring, not just the ball: the planet cuts through the orbital plane, and
where the browser splits it there, it dropped any ring that stuck out of the box */
left: calc(50% - var(--s) * var(--u) * 1.1);
width: calc(var(--s) * var(--u) * 2.2);
background: none; /* the ball is the <i>, so it can sit between the two halves */
}
.ringed i {
position: absolute;
inset: 0 calc(var(--s) * var(--u) * 0.6);
}
.orbit .ringed::before,
.orbit .ringed::after {
box-sizing: content-box; /* each ring is sized by its hole, its border drawn outside it */
content: '';
position: absolute;
top: 50%;
left: 50%;
width: calc(var(--s) * var(--u) * 1.8);
height: calc(var(--s) * var(--u) / 2);
border: calc(2 * var(--u)) solid #ffd79a;
/* the same pale gold on a dark stage, a deeper gold on a light one: its lightness follows the
stage's text colour, which the model inherits */
border-color: oklch(from currentColor calc(0.75 + l * 0.16) calc(0.19 - l * 0.09) 75);
border-radius: 50%;
transform: translate(-50%, -50%) rotate(-18deg);
}
.orbit .ringed::before {
clip-path: inset(0 0 50% 0);
}
.orbit .ringed::after {
clip-path: inset(50% 0 0 0);
}
@keyframes orbit {
to { transform: rotateZ(360deg); }
}
/* undo the ring's spin, then the plane's tilt, then its roll */
@keyframes face {
from { transform: rotateZ(0deg) rotateX(-56deg) rotateZ(16deg); }
to { transform: rotateZ(-360deg) rotateX(-56deg) rotateZ(16deg); }
}