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.rotateX + rotateZ isometric viewroof translateZ, walls on ::before / ::afterwindows from layered gradientspointer → --rx / --rz<div class="city">
<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>
<b class="hint">Move your pointer</b>
</div>
.city {
position: fixed;
inset: 0;
display: grid;
place-items: center;
overflow: hidden;
perspective: 900px;
}
.world {
--cell: 40px; /* grid pitch */
--line: rgb(255 181 71 / 0.4);
position: relative;
width: 172px;
height: 172px;
font-size: 17px; /* 1em of building height */
border: 1px solid rgb(139 108 255 / 0.45);
border-radius: 10px;
/* street centre lines, one per 40px, in the gaps between the lots */
background:
repeating-linear-gradient(90deg, transparent 0 5px, var(--line) 5px 7px, transparent 7px 40px),
repeating-linear-gradient(transparent 0 5px, var(--line) 5px 7px, transparent 7px 40px),
#1f1f40;
transform-style: preserve-3d;
transform: translateY(26px) 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(12px + var(--x) * var(--cell));
top: calc(12px + var(--y) * var(--cell));
width: 28px;
height: 28px;
background: color-mix(in srgb, var(--c) 62%, #141830);
box-shadow: inset 0 0 0 3px 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 4px, transparent 4px 8px),
repeating-linear-gradient(90deg, var(--wall) 0 4px, var(--lit) 4px 8px);
background-position: 0 2px, 2px 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 4px, transparent 4px 8px),
repeating-linear-gradient(var(--wall) 0 4px, var(--lit) 4px 8px);
background-position: 0 0, 2px 0, 0 2px;
transform-origin: left;
transform: rotateY(90deg);
}
/* the hint is a flat overlay, outside the 3D world */
.hint {
position: absolute;
inset: auto 0 12px;
color: #949bc0;
font: 600 13px system-ui;
text-align: center;
pointer-events: none;
transition: opacity 0.4s;
}
.is-touched .hint {
opacity: 0;
}
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');
});
// Isometric block of 16 buildings. One element per building:
// the element itself = the roof, lifted by translateZ(height)
// ::before = the wall hanging down from its +y edge (rotateX(-90deg) around that edge)
// ::after = the wall hanging down from its +x edge (rotateY(90deg) around that edge)
// With rotateZ(45deg) those two walls are the ones that face the camera.
// Heights are in em (--h), so the font-size scales every building at once.
$cell: 40px; // grid pitch
$lot: 28px; // building footprint
$pad: 12px; // ground margin around the block
.d-city {
position: absolute;
inset: 0;
display: grid;
place-items: center;
overflow: hidden;
perspective: 900px;
&__world {
position: relative;
width: $cell * 4 + $pad * 2 - ($cell - $lot);
height: $cell * 4 + $pad * 2 - ($cell - $lot);
font-size: 17px;
border-radius: 10px;
// Street centre lines. Lots start at $pad and repeat every $cell, so a line at
// ($pad - 6px) mod $cell lands in the middle of every street, and around the block.
--line: color-mix(in srgb, var(--warm) 40%, transparent);
background:
repeating-linear-gradient(90deg, transparent 0 5px, var(--line) 5px 7px, transparent 7px $cell),
repeating-linear-gradient(transparent 0 5px, var(--line) 5px 7px, transparent 7px $cell),
color-mix(in srgb, var(--accent) 16%, var(--surface-solid));
border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
transform-style: preserve-3d;
// translateY first: the buildings rise upward, so the block sits a bit low in the frame
transform: translateY(26px) 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);
// while the pointer drives, follow it almost instantly
&.is-live {
transition-duration: 0.1s;
}
}
// the roof
i {
--wall: color-mix(in srgb, var(--c) 42%, var(--surface-solid));
--lit: color-mix(in srgb, var(--warm) 75%, #fff);
position: absolute;
left: calc(#{$pad} + var(--x) * #{$cell});
top: calc(#{$pad} + var(--y) * #{$cell});
width: $lot;
height: $lot;
background: color-mix(in srgb, var(--c) 62%, var(--surface-solid));
box-shadow: inset 0 0 0 3px color-mix(in srgb, var(--c) 40%, var(--surface-solid));
transform-style: preserve-3d;
transform: translateZ(calc(var(--h) * 1em));
// Windows: horizontal wall bands (top layer) over vertical window columns (bottom layer).
&::before,
&::after {
content: '';
position: absolute;
}
// +y wall: hangs from the bottom edge
&::before {
top: 100%;
left: 0;
width: 100%;
height: calc(var(--h) * 1em);
background:
repeating-linear-gradient(var(--wall) 0 4px, transparent 4px 8px),
repeating-linear-gradient(90deg, var(--wall) 0 4px, var(--lit) 4px 8px);
background-position: 0 2px, 2px 0;
transform-origin: top;
transform: rotateX(-90deg);
}
// +x wall: hangs from the right edge. Its own x axis now points DOWN the building,
// so the window pattern is turned by 90deg. It gets a shade darker.
&::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 4px, transparent 4px 8px),
repeating-linear-gradient(var(--wall) 0 4px, var(--lit) 4px 8px);
background-position: 0 0, 2px 0, 0 2px;
transform-origin: left;
transform: rotateY(90deg);
}
}
// The hint is a flat overlay, outside the tilted world.
b {
position: absolute;
inset: auto 0 12px;
color: var(--muted);
font-size: 13px;
font-weight: 600;
text-align: center;
pointer-events: none;
transition: opacity 0.4s;
}
&.is-touched b {
opacity: 0;
}
}