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.
Layers sit at different translateZ depths; tilting the scene with the pointer makes them slide apart.
Your edited version
translateZ: moon far away (−300px), mountains, hills, trees in front (+70px).scale() to keep filling the frame.inset: -20%) so their edges never show while tilting.translateZ per layerscale() to compensate for distancepointer → two rotation variables<div class="view">
<div class="world">
<i></i><i></i><i></i><i></i>
</div>
</div>
.view {
position: fixed;
inset: 0;
overflow: hidden;
perspective: 500px;
background: linear-gradient(#0c1033, #3a1d5e 60%, #ff7a59);
}
.world {
position: absolute;
inset: 0;
transform-style: preserve-3d;
transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
/* slow ease back to rest... */
transition: transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
}
/* ...but nearly instant while the pointer is driving: a long transition restarts on every
pointer event and makes the scene lag behind the hand */
.world.is-live {
transition-duration: 0.1s;
}
.world i {
position: absolute;
inset: -20%;
}
/* moon */
.world i:nth-child(1) {
inset: 14% auto auto 62%;
width: 70px;
height: 70px;
border-radius: 50%;
background: #fff4d1;
box-shadow: 0 0 40px #fff4d1;
transform: translateZ(-300px) scale(1.6);
}
/* far mountains */
.world i:nth-child(2) {
background: #4a2a73;
clip-path: polygon(0 100%, 0 62%, 18% 44%, 34% 60%, 52% 38%, 70% 58%, 86% 42%, 100% 60%, 100% 100%);
transform: translateZ(-160px) scale(1.32);
}
/* near hills */
.world i:nth-child(3) {
background: #2a1648;
clip-path: polygon(0 100%, 0 70%, 22% 58%, 44% 72%, 66% 56%, 84% 70%, 100% 62%, 100% 100%);
transform: translateZ(-40px) scale(1.08);
}
/* foreground trees */
.world i:nth-child(4) {
background: #0d0820;
clip-path: polygon(0 100%, 0 78%, 6% 78%, 10% 60%, 14% 78%, 80% 80%, 85% 58%, 90% 80%, 100% 80%, 100% 100%);
transform: translateZ(70px) scale(0.9);
}
const view = document.querySelector('.view');
const world = document.querySelector('.world');
view.addEventListener('pointermove', (e) => {
const x = e.clientX / innerWidth - 0.5; // -0.5 … 0.5
const y = e.clientY / innerHeight - 0.5;
world.style.setProperty('--ry', x * 30 + 'deg');
world.style.setProperty('--rx', -y * 20 + 'deg');
world.classList.add('is-live');
});
view.addEventListener('pointerleave', () => {
world.classList.remove('is-live');
world.style.removeProperty('--rx');
world.style.removeProperty('--ry');
});
.d-parallax {
position: absolute;
inset: 0;
overflow: hidden;
perspective: 500px;
background: linear-gradient(#0c1033, #3a1d5e 60%, #ff7a59);
&__world {
position: absolute;
inset: 0;
transform-style: preserve-3d;
transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
// Slow ease back to rest. While the pointer is driving, the transition is almost off:
// a long one restarts on every pointer event and makes the scene lag behind the hand.
transition: transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
&.is-live {
transition-duration: 0.1s;
}
}
// Far layers are pushed back AND scaled up so they still fill the frame.
i {
position: absolute;
inset: -20%;
}
// moon
i:nth-child(1) {
inset: 14% auto auto 62%;
width: 46px;
height: 46px;
border-radius: 50%;
background: #fff4d1;
box-shadow: 0 0 40px #fff4d1;
transform: translateZ(-300px) scale(1.6);
}
// far mountains
i:nth-child(2) {
background: #4a2a73;
clip-path: polygon(0 100%, 0 62%, 18% 44%, 34% 60%, 52% 38%, 70% 58%, 86% 42%, 100% 60%, 100% 100%);
transform: translateZ(-160px) scale(1.32);
}
// near hills
i:nth-child(3) {
background: #2a1648;
clip-path: polygon(0 100%, 0 70%, 22% 58%, 44% 72%, 66% 56%, 84% 70%, 100% 62%, 100% 100%);
transform: translateZ(-40px) scale(1.08);
}
// foreground trees
i:nth-child(4) {
background: #0d0820;
clip-path: polygon(0 100%, 0 78%, 6% 78%, 10% 60%, 14% 78%, 80% 80%, 85% 58%, 90% 80%, 100% 80%, 100% 100%);
transform: translateZ(70px) scale(0.9);
}
// The hint is a flat overlay, NOT part of the 3D world: inside the world, near the bottom edge
// and close to the camera, perspective pushed it out of frame until the scene tilted.
b {
position: absolute;
inset: auto 0 12px;
color: #fff;
font-size: 13px;
font-weight: 600;
text-align: center;
text-shadow: 0 1px 6px rgb(0 0 0 / 0.7);
pointer-events: none;
transition: opacity 0.4s;
}
// once the visitor has moved the scene, the hint has done its job
&.is-touched b {
opacity: 0;
}
}