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.
A box seen from the inside: floor, ceiling and three walls. The pointer rotates it around the camera position, so it feels like turning your head.
Your edited version
translateZ, and side walls, floor and ceiling folded 90° from the edges of the view. Each is turned so its front faces into the room.translateZ(110px), in front of the screen, and run 460px deep. That extra length toward the camera is what you see when you turn your head; stop well short of the perspective distance (300px).transform-origin: 50% 50% 300px, the same as the perspective. The box then rotates around the viewer’s eye, which reads as looking around, not as the room swinging.--rx / --ry of a few degrees. Everything on the walls (window, poster, door, rug, lamp) is a flat child of its wall, drawn with gradients.box faces turned inwardtransform-origin z = perspectivewalls extended toward the camerapointer → --rx / --ry<div class="room">
<div class="box">
<div class="wall back"><i class="window"></i><i class="picture"></i></div>
<div class="wall left"><i class="poster"></i></div>
<div class="wall right"><i class="door"></i></div>
<div class="wall floor"><i class="light"></i><i class="rug"></i></div>
<div class="wall ceil"><i class="lamp"></i></div>
</div>
<b class="hint">Move your pointer</b>
</div>
.room {
position: fixed;
inset: 0;
overflow: hidden;
perspective: 300px;
background: #06070d;
}
.room * {
box-sizing: border-box;
}
.box {
position: absolute;
inset: 0;
transform-style: preserve-3d;
transform-origin: 50% 50% 300px; /* rotate around the camera = turn your head */
transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
transition: transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.box.is-live {
transition-duration: 0.12s;
}
.wall {
position: absolute;
background-color: #28244a;
}
.wall > i {
position: absolute;
}
/* 110px (near end) - 460px (depth) = -350px */
.back {
inset: 0;
background-image: radial-gradient(ellipse 70% 60% at 50% 0%, rgb(255 181 71 / 0.2), transparent 70%);
transform: translateZ(-350px);
}
.left,
.right {
top: 0;
width: 460px;
height: 100%;
}
.left {
left: 0;
background-image: linear-gradient(90deg, rgb(0 0 0 / 0.35), rgb(0 0 0 / 0.08));
transform-origin: left;
transform: translateZ(110px) rotateY(90deg);
}
.right {
right: 0;
background-image: linear-gradient(270deg, rgb(0 0 0 / 0.4), rgb(0 0 0 / 0.12));
transform-origin: right;
transform: translateZ(110px) rotateY(-90deg);
}
.floor,
.ceil {
left: 0;
width: 100%;
height: 460px;
}
.floor {
bottom: 0;
background:
linear-gradient(transparent 30%, rgb(0 0 0 / 0.45)),
repeating-linear-gradient(90deg, transparent 0 30px, rgb(0 0 0 / 0.3) 30px 32px),
#6e4f2e;
transform-origin: bottom;
transform: translateZ(110px) rotateX(90deg);
}
.ceil {
top: 0;
background-image: linear-gradient(rgb(0 0 0 / 0.4), rgb(0 0 0 / 0.1));
transform-origin: top;
transform: translateZ(110px) rotateX(-90deg);
}
/* back wall */
.window {
top: 20%;
left: 16%;
width: 30%;
height: 40%;
border: 5px solid #948fb0;
border-radius: 3px;
background:
linear-gradient(90deg, transparent calc(50% - 2px), #948fb0 0 calc(50% + 2px), transparent 0),
linear-gradient(transparent calc(50% - 2px), #948fb0 0 calc(50% + 2px), transparent 0),
radial-gradient(circle at 72% 28%, #fff7da 0 9%, rgb(255 247 218 / 0.3) 10%, transparent 24%),
radial-gradient(circle at 20% 30%, #fff 0 1px, transparent 2px),
radial-gradient(circle at 36% 70%, #fff 0 1px, transparent 2px),
linear-gradient(#070b24, #2b2466);
}
.picture {
top: 24%;
left: 60%;
width: 22%;
height: 28%;
border: 4px solid #ffb547;
background: linear-gradient(160deg, #2ee6d6, #8b6cff 55%, #ff4d9d);
}
/* left wall: its left end is near the camera */
.poster {
top: 22%;
left: 50%;
width: 22%;
height: 38%;
background:
radial-gradient(circle at 50% 38%, #ffb547 0 18%, transparent 19%),
linear-gradient(transparent 62%, #ff4d9d 62% 70%, transparent 70%),
linear-gradient(#8b6cff, #463680);
}
/* right wall: its RIGHT end is near the camera */
.door {
bottom: 0;
left: 14%;
width: 18%;
height: 64%;
border: 4px solid #4a3220;
border-bottom: 0;
background:
radial-gradient(circle at 18% 54%, #ffb547 0 3px, transparent 4px),
#7e5a33;
}
/* floor: its top edge touches the back wall */
.light {
top: 4%;
left: 22%;
width: 30%;
height: 34%;
background: linear-gradient(rgb(223 230 255 / 0.22), transparent);
clip-path: polygon(8% 0, 92% 0, 100% 100%, 0 100%);
}
.rug {
top: 30%;
left: 24%;
width: 52%;
height: 34%;
border-radius: 50%;
background: repeating-radial-gradient(ellipse, #ff4d9d 0 8px, #463680 8px 16px, #2ee6d6 16px 19px, #463680 19px 26px);
opacity: 0.85;
}
/* ceiling: its top edge is near the camera */
.lamp {
top: 50%;
left: calc(50% - 45px);
width: 90px;
height: 90px;
border-radius: 50%;
background: radial-gradient(circle, #fffaf0 0 16%, rgb(255 181 71 / 0.55) 20%, transparent 68%);
}
/* the hint is a flat overlay, outside the 3D world */
.hint {
position: absolute;
inset: auto 0 12px;
color: #fff;
font: 600 13px system-ui;
text-align: center;
pointer-events: none;
transition: opacity 0.4s;
}
.is-touched .hint {
opacity: 0;
}
const view = document.querySelector('.room');
const target = view.querySelector('.box');
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('--ry', x * 16 + 'deg');
target.style.setProperty('--rx', -y * 10 + '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');
});
// A box seen from the inside. The box fills the stage; every face is turned so its front
// looks INTO the room. The side walls, floor and ceiling are $depth long and start $front
// in front of the screen plane, so turning the head never reveals their near edges.
// The box rotates around the camera itself (transform-origin z = perspective).
$persp: 300px;
$depth: 460px;
$front: 110px; // stays well below $persp: nothing may reach the camera
.d-room {
position: absolute;
inset: 0;
overflow: hidden;
perspective: $persp;
background: #06070d;
&__box {
--wall: color-mix(in srgb, var(--accent) 16%, var(--surface-solid));
position: absolute;
inset: 0;
transform-style: preserve-3d;
transform-origin: 50% 50% $persp;
transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
transition: transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
&.is-live {
transition-duration: 0.12s;
}
}
&__wall {
position: absolute;
background-color: var(--wall);
> i {
position: absolute;
}
// far wall: the whole stage, pushed back
&--back {
inset: 0;
background-image: radial-gradient(ellipse 70% 60% at 50% 0%, color-mix(in srgb, var(--warm) 22%, transparent), transparent 70%);
transform: translateZ($front - $depth);
}
// side walls: element's left/right edge is the near end, the other end is at the back wall
&--left,
&--right {
top: 0;
width: $depth;
height: 100%;
}
&--left {
left: 0;
background-image: linear-gradient(90deg, rgb(0 0 0 / 0.35), rgb(0 0 0 / 0.08));
transform-origin: left;
transform: translateZ($front) rotateY(90deg);
}
&--right {
right: 0;
background-image: linear-gradient(270deg, rgb(0 0 0 / 0.4), rgb(0 0 0 / 0.12));
transform-origin: right;
transform: translateZ($front) rotateY(-90deg);
}
&--floor,
&--ceil {
left: 0;
width: 100%;
height: $depth;
}
// floor: wooden boards running toward the back wall, darker near the camera
&--floor {
bottom: 0;
background:
linear-gradient(transparent 30%, rgb(0 0 0 / 0.45)),
repeating-linear-gradient(90deg, transparent 0 30px, rgb(0 0 0 / 0.3) 30px 32px),
color-mix(in srgb, var(--warm) 34%, #2a1c12);
transform-origin: bottom;
transform: translateZ($front) rotateX(90deg);
}
&--ceil {
top: 0;
background-image: linear-gradient(rgb(0 0 0 / 0.4), rgb(0 0 0 / 0.1));
transform-origin: top;
transform: translateZ($front) rotateX(-90deg);
}
}
// ---- back wall ----
// night window: moon, a few stars, a frame and a cross bar
&__window {
top: 20%;
left: 16%;
width: 30%;
height: 40%;
border: 5px solid color-mix(in srgb, var(--wall) 60%, #fff);
border-radius: 3px;
background:
linear-gradient(90deg, transparent calc(50% - 2px), color-mix(in srgb, var(--wall) 60%, #fff) 0 calc(50% + 2px), transparent 0),
linear-gradient(transparent calc(50% - 2px), color-mix(in srgb, var(--wall) 60%, #fff) 0 calc(50% + 2px), transparent 0),
radial-gradient(circle at 72% 28%, #fff7da 0 9%, color-mix(in srgb, #fff7da 30%, transparent) 10%, transparent 24%),
radial-gradient(circle at 20% 30%, #fff 0 1px, transparent 2px),
radial-gradient(circle at 36% 70%, #fff 0 1px, transparent 2px),
radial-gradient(circle at 84% 76%, #fff 0 1px, transparent 2px),
linear-gradient(#070b24, color-mix(in srgb, var(--accent) 40%, #0b1030));
}
&__picture {
top: 24%;
left: 60%;
width: 22%;
height: 28%;
border: 4px solid var(--warm);
background:
radial-gradient(circle at 68% 34%, #fff2 0 12%, transparent 13%),
linear-gradient(160deg, var(--accent-2), var(--accent) 55%, var(--hot));
box-shadow: 0 6px 14px rgb(0 0 0 / 0.35);
}
// ---- left wall (its left end is near the camera) ----
&__poster {
top: 22%;
left: 50%;
width: 22%;
height: 38%;
border-radius: 2px;
background:
radial-gradient(circle at 50% 38%, var(--warm) 0 18%, transparent 19%),
linear-gradient(transparent 62%, var(--hot) 62% 70%, transparent 70%),
linear-gradient(var(--accent), color-mix(in srgb, var(--accent) 50%, #000));
}
// ---- right wall (its RIGHT end is near the camera, so the far end is at left: 0) ----
&__door {
bottom: 0;
left: 14%;
width: 18%;
height: 64%;
border: 4px solid color-mix(in srgb, var(--warm) 30%, #1b120b);
border-bottom: 0;
background:
radial-gradient(circle at 18% 54%, var(--warm) 0 3px, transparent 4px),
linear-gradient(transparent 12%, rgb(0 0 0 / 0.18) 12% 44%, transparent 44% 52%, rgb(0 0 0 / 0.18) 52% 90%, transparent 90%),
color-mix(in srgb, var(--warm) 40%, #3b2616);
}
// ---- floor (its top edge is at the back wall) ----
// moonlight falling through the window
&__light {
top: 4%;
left: 22%;
width: 30%;
height: 34%;
background: linear-gradient(color-mix(in srgb, #dfe6ff 22%, transparent), transparent);
clip-path: polygon(8% 0, 92% 0, 100% 100%, 0 100%);
}
&__rug {
top: 30%;
left: 24%;
width: 52%;
height: 34%;
border-radius: 50%;
background: repeating-radial-gradient(
ellipse,
var(--hot) 0 8px,
color-mix(in srgb, var(--accent) 70%, #000) 8px 16px,
var(--accent-2) 16px 19px,
color-mix(in srgb, var(--accent) 70%, #000) 19px 26px
);
opacity: 0.85;
}
// ---- ceiling (its top edge is near the camera) ----
&__lamp {
top: 50%;
left: calc(50% - 45px);
width: 90px;
height: 90px;
border-radius: 50%;
background: radial-gradient(circle, #fffaf0 0 16%, color-mix(in srgb, var(--warm) 55%, transparent) 20%, transparent 68%);
}
// The hint is a flat overlay, outside the rotating box.
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;
}
&.is-touched b {
opacity: 0;
}
}