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(68deg) 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(-68deg) 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.rotateX tilted planerotateZ orbit per ringcounter-rotation = billboardingnegative animation-delay<div class="scene">
<div class="system">
<div class="sun"></div>
<div class="orbit" style="--r:34px;--t:5s;--s:9px;--c:#2ee6d6;--d:-1s"><b></b></div>
<div class="orbit" style="--r:54px;--t:9s;--s:13px;--c:#ff4d9d;--d:-6s"><b></b></div>
<div class="orbit" style="--r:76px;--t:14s;--s:11px;--c:#8b6cff;--d:-3s"><b></b></div>
<div class="orbit" style="--r:98px;--t:22s;--s:16px;--c:#ffb547;--d:-15s"><b class="ringed"></b></div>
</div>
</div>
.scene {
perspective: 800px;
}
.system,
.system * {
box-sizing: border-box;
}
/* the orbital plane: rolled on screen, then laid back */
.system {
position: relative;
width: 200px;
height: 200px;
transform-style: preserve-3d;
transform: rotateZ(-16deg) rotateX(68deg);
}
.sun {
position: absolute;
inset: calc(50% - 13px);
border-radius: 50%;
background: radial-gradient(circle at 38% 34%, #fff8e1, #ffb547 45%, #ff4d9d);
box-shadow: 0 0 16px #ffb547, 0 0 40px rgb(255 77 157 / 0.45);
transform: rotateX(-68deg) rotateZ(16deg); /* the plane, undone: faces the camera */
}
.orbit {
position: absolute;
inset: calc(50% - var(--r));
border: 1px 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: -2px;
border-radius: 50%;
background: conic-gradient(transparent 62%, var(--c));
mask: radial-gradient(closest-side, transparent calc(100% - 3px), #000 calc(100% - 2px));
}
.orbit b {
position: absolute;
top: calc(var(--s) / -2);
left: calc(50% - var(--s) / 2);
width: var(--s);
height: var(--s);
border-radius: 50%;
background: radial-gradient(circle at 34% 30%, #fff 0 6%, var(--c) 42%, color-mix(in srgb, var(--c) 45%, #000));
/* same duration + delay as the ring: the two rotations cancel */
animation: face var(--t) linear var(--d) infinite;
}
/* a flat ring; hiding its top border makes the far half pass "behind" */
.orbit .ringed::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 180%;
height: 50%;
border: 2px solid #ffd79a;
border-top-color: transparent;
border-radius: 50%;
transform: translate(-50%, -50%) rotate(-18deg);
}
@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(-68deg) rotateZ(16deg); }
to { transform: rotateZ(-360deg) rotateX(-68deg) rotateZ(16deg); }
}
// The plane is laid back with rotateX and turned a little on screen with rotateZ.
// Each orbit ring spins on Z inside that plane; its planet undoes the spin AND the plane's
// tilt, in reverse order, so it always faces the camera (billboarding).
$tilt: 68deg;
$roll: -16deg;
$face: rotateX(-$tilt) rotateZ(-$roll); // the inverse of the plane, applied last
.d-solar {
position: relative;
width: 200px;
height: 200px;
transform-style: preserve-3d;
transform: rotateZ($roll) rotateX($tilt);
&__sun {
position: absolute;
inset: calc(50% - 13px);
border-radius: 50%;
background: radial-gradient(circle at 38% 34%, #fff8e1, var(--warm) 45%, var(--hot) 100%);
box-shadow:
0 0 16px color-mix(in srgb, var(--warm) 80%, transparent),
0 0 40px color-mix(in srgb, var(--hot) 45%, transparent);
transform: $face;
// a soft corona that breathes (opacity + scale only)
&::after {
content: '';
position: absolute;
inset: -9px;
border-radius: 50%;
background: radial-gradient(circle, color-mix(in srgb, var(--warm) 55%, transparent) 40%, transparent 70%);
animation: d-solar-glow 3s ease-in-out infinite alternate;
}
}
&__orbit {
position: absolute;
inset: calc(50% - var(--r));
border: 1px solid color-mix(in srgb, var(--c) 30%, transparent);
border-radius: 50%;
transform-style: preserve-3d;
animation: d-solar-orbit var(--t) linear var(--d) infinite;
// A fading trail on the ring behind the planet: a conic gradient cut to a thin ring.
// It has no children, so the mask cannot flatten anything.
&::before {
content: '';
position: absolute;
inset: -2px;
border-radius: 50%;
background: conic-gradient(transparent 62%, color-mix(in srgb, var(--c) 85%, transparent));
mask: radial-gradient(closest-side, transparent calc(100% - 3px), #000 calc(100% - 2px));
}
b {
position: absolute;
top: calc(var(--s) / -2);
left: calc(50% - var(--s) / 2);
width: var(--s);
height: var(--s);
border-radius: 50%;
background: radial-gradient(
circle at 34% 30%,
#fff 0 6%,
var(--c) 42%,
color-mix(in srgb, var(--c) 45%, #000) 100%
);
box-shadow: 0 0 10px color-mix(in srgb, var(--c) 55%, transparent);
// same duration and delay as the ring, so the two rotations cancel at every moment
animation: d-solar-face var(--t) linear var(--d) infinite;
}
// Saturn-ish: the ring is 2D (the planet always faces us). Hiding the top border
// makes the ring's far half look like it passes behind the planet.
.is-ringed::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 180%;
height: 50%;
border: 2px solid color-mix(in srgb, var(--c) 80%, #fff);
border-top-color: transparent;
border-radius: 50%;
transform: translate(-50%, -50%) rotate(-18deg);
}
}
}
@keyframes d-solar-orbit {
to {
transform: rotateZ(360deg);
}
}
// rotateZ(-angle) undoes the ring; then the plane's tilt and roll are undone
@keyframes d-solar-face {
from {
transform: rotateZ(0deg) $face;
}
to {
transform: rotateZ(-360deg) $face;
}
}
@keyframes d-solar-glow {
from {
opacity: 0.55;
transform: scale(0.92);
}
to {
opacity: 1;
transform: scale(1.12);
}
}