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.
Sixteen 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.
Your edited version
rotateX(58deg) rotateZ(45deg). Every building is placed on it with left/top from --x/--y.translateZ(var(--h)). Heights are in em, so one font-size scales the whole skyline.::before hangs from the roof’s bottom edge (transform-origin: top; rotateX(-90deg)) and ::after from its right edge (transform-origin: left; rotateY(90deg)). With a 45° turn those are exactly the two walls you can see; the hidden two are never built.--rx / --rz; a transition eases the block there, fast while the pointer moves and slow on the way back.--u, tied to the canvas: the ground is 172 units square and one em of height is 17 of them, so the city is the same share of a gallery card, the editor and a recording canvas. The whole canvas stays the pointer’s field and the block stands in the middle of it. There is no hint line: the badge in the corner of the stage already says to move the pointer.rotateX + rotateZ isometric viewroof translateZ, walls on ::before / ::afterwindows from layered gradientspointer → --rx / --rz<div class="city" tabindex="0" role="img" aria-label="Isometric city block that turns toward the pointer">
<div class="view">
<div class="world">
<i style="--x:0;--y:0;--h:3.4;--c:#8b6cff"></i>
<i style="--x:1;--y:0;--h:2.6;--c:#2ee6d6"></i>
<i style="--x:2;--y:0;--h:3;--c:#ff4d9d"></i>
<i style="--x:3;--y:0;--h:1.8;--c:#8b6cff"></i>
<i style="--x:0;--y:1;--h:2.4;--c:#ffb547"></i>
<i style="--x:1;--y:1;--h:1.2;--c:#8b6cff"></i>
<i style="--x:2;--y:1;--h:2.2;--c:#2ee6d6"></i>
<i style="--x:3;--y:1;--h:1.4;--c:#ff4d9d"></i>
<i style="--x:0;--y:2;--h:2.8;--c:#8b6cff"></i>
<i style="--x:1;--y:2;--h:1.8;--c:#2ee6d6"></i>
<i style="--x:2;--y:2;--h:0.9;--c:#ff4d9d"></i>
<i style="--x:3;--y:2;--h:1.6;--c:#8b6cff"></i>
<i style="--x:0;--y:3;--h:1.5;--c:#ffb547"></i>
<i style="--x:1;--y:3;--h:1.1;--c:#8b6cff"></i>
<i style="--x:2;--y:3;--h:1.3;--c:#2ee6d6"></i>
<i style="--x:3;--y:3;--h:0.6;--c:#ff4d9d"></i>
</div>
</div>
</div>
/* the whole canvas is the pointer's field; the block itself stands in the model box */
.city {
/* one base unit: every length in the block is a multiple of it, so the city is the same share
of a gallery card, the editor, a full screen and a recording canvas */
--u: 0.33vmin;
position: fixed;
inset: 0;
display: grid;
place-items: center;
overflow: hidden;
}
/* the model box */
.view {
height: 70vmin;
display: grid;
place-items: center;
perspective: calc(900 * var(--u));
}
.world {
box-sizing: content-box; /* its size is its inside, its edge outside it, on any page */
--cell: calc(40 * var(--u)); /* grid pitch */
--line: rgb(255 181 71 / 0.4);
position: relative;
width: calc(172 * var(--u));
height: calc(172 * var(--u));
font-size: calc(17 * var(--u)); /* 1em of building height */
border: calc(1 * var(--u)) solid rgb(139 108 255 / 0.45);
border-radius: calc(10 * var(--u));
/* street centre lines, one per 40 units, in the gaps between the lots */
background:
repeating-linear-gradient(90deg, transparent 0 calc(5 * var(--u)), var(--line) calc(5 * var(--u)) calc(7 * var(--u)), transparent calc(7 * var(--u)) calc(40 * var(--u))),
repeating-linear-gradient(transparent 0 calc(5 * var(--u)), var(--line) calc(5 * var(--u)) calc(7 * var(--u)), transparent calc(7 * var(--u)) calc(40 * var(--u))),
#1f1f40;
transform-style: preserve-3d;
transform: translateY(calc(8 * var(--u))) rotateX(calc(58deg + var(--rx, 0deg))) rotateZ(calc(45deg + var(--rz, 0deg)));
transition: transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.world.is-live {
transition-duration: 0.1s;
}
/* the roof */
.world i {
--wall: color-mix(in srgb, var(--c) 42%, #141830);
--lit: #ffd89a;
position: absolute;
left: calc(calc(12 * var(--u)) + var(--x) * var(--cell));
top: calc(calc(12 * var(--u)) + var(--y) * var(--cell));
width: calc(28 * var(--u));
height: calc(28 * var(--u));
background: color-mix(in srgb, var(--c) 62%, #141830);
box-shadow: inset 0 0 0 calc(3 * var(--u)) color-mix(in srgb, var(--c) 40%, #141830);
transform-style: preserve-3d;
transform: translateZ(calc(var(--h) * 1em));
}
.world i::before,
.world i::after {
content: '';
position: absolute;
}
/* front wall: hangs down from the bottom edge */
.world i::before {
top: 100%;
left: 0;
width: 100%;
height: calc(var(--h) * 1em);
background:
repeating-linear-gradient(var(--wall) 0 calc(4 * var(--u)), transparent calc(4 * var(--u)) calc(8 * var(--u))),
repeating-linear-gradient(90deg, var(--wall) 0 calc(4 * var(--u)), var(--lit) calc(4 * var(--u)) calc(8 * var(--u)));
background-position: 0 calc(2 * var(--u)), calc(2 * var(--u)) 0;
transform-origin: top;
transform: rotateX(-90deg);
}
/* side wall: hangs down from the right edge, pattern turned, a shade darker */
.world i::after {
top: 0;
left: 100%;
width: calc(var(--h) * 1em);
height: 100%;
background:
linear-gradient(rgb(0 0 0 / 0.28), rgb(0 0 0 / 0.28)),
repeating-linear-gradient(90deg, var(--wall) 0 calc(4 * var(--u)), transparent calc(4 * var(--u)) calc(8 * var(--u))),
repeating-linear-gradient(var(--wall) 0 calc(4 * var(--u)), var(--lit) calc(4 * var(--u)) calc(8 * var(--u)));
background-position: 0 0, calc(2 * var(--u)) 0, 0 calc(2 * var(--u));
transform-origin: left;
transform: rotateY(90deg);
}
/* the keyboard: Tab to it and it leans as it would for a pointer near the top right, even with
the pointer resting on the canvas (!important beats the pose the script writes inline) */
.city:focus-visible .world { --rx: -4deg !important; --rz: -10deg !important; }
const view = document.querySelector('.city');
const target = view.querySelector('.world');
view.addEventListener('pointermove', (e) => {
const r = view.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width - 0.5; // -0.5 … 0.5
const y = (e.clientY - r.top) / r.height - 0.5;
target.style.setProperty('--rz', -x * 40 + 'deg');
target.style.setProperty('--rx', -y * 16 + 'deg');
target.classList.add('is-live');
view.classList.add('is-touched');
});
view.addEventListener('pointerleave', () => {
target.classList.remove('is-live');
target.style.removeProperty('--rx');
target.style.removeProperty('--ry');
target.style.removeProperty('--rz');
});