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.
JS creates forty flakes once, each with a random depth, speed and drift in custom properties. One CSS animation does the falling; perspective makes near flakes big and fast.
Your edited version
--x, depth --z, size, opacity, sideways --drift, duration --t and delay --d. After that it does nothing.translate3d(0, -160px, var(--z)) → translate3d(var(--drift), 560px, var(--z)). Perspective makes near flakes big and fast, far ones small and slow.random --x / --z / --t / --d per flaketranslate3d keyframes with var()negative delays = already snowingtrees at real depths hide far flakes<div class="night">
<div class="moon"></div>
<div class="hill"></div>
<div class="world">
<b style="--x:8%;--z:-240px"></b>
<b style="--x:66%;--z:-140px"></b>
<b style="--x:34%;--z:-20px"></b>
</div>
</div>
.night {
position: fixed;
inset: 0;
overflow: hidden;
perspective: 400px;
background: linear-gradient(#050817, #1f1d5a 75%, #1a2e4a);
}
.moon {
position: absolute;
top: 12%;
right: 16%;
width: 38px;
height: 38px;
border-radius: 50%;
background: radial-gradient(circle at 60% 40%, #fffbe8, #e6e2ff 70%);
box-shadow: 0 0 24px rgb(255 250 230 / 0.55), 0 0 70px rgb(139 108 255 / 0.45);
}
.hill {
position: absolute;
left: -20%;
right: -20%;
bottom: -32%;
height: 62%;
border-radius: 50% 50% 0 0 / 34% 34% 0 0;
background: linear-gradient(#eef1ff, #b9c0e8 40%, #8d95c8);
}
.world {
position: absolute;
inset: 0;
transform-style: preserve-3d;
pointer-events: none;
}
/* pine trees at real depths */
.world b {
position: absolute;
bottom: 3%;
left: var(--x);
width: 70px;
height: 104px;
background:
linear-gradient(transparent 26%, #e9ecff 26% 30%, transparent 30% 55%, #e9ecff 55% 59%, transparent 59%),
linear-gradient(#135a55, #07161c);
clip-path: polygon(50% 0, 72% 28%, 62% 28%, 84% 57%, 70% 57%, 96% 88%, 56% 88%, 56% 100%, 44% 100%, 44% 88%, 4% 88%, 30% 57%, 16% 57%, 38% 28%, 28% 28%);
transform: translateZ(var(--z));
}
.world i {
position: absolute;
top: 0;
left: var(--x);
width: var(--s);
height: var(--s);
border-radius: 50%;
background: radial-gradient(circle, #fff 0 35%, rgb(255 255 255 / 0) 70%);
opacity: var(--o);
animation: fall var(--t) linear var(--d) infinite;
}
@keyframes fall {
from { transform: translate3d(0, -160px, var(--z)); }
to { transform: translate3d(var(--drift), 560px, var(--z)); }
}
const world = document.querySelector('.world');
const rand = (min, max) => min + Math.random() * (max - min);
for (let n = 0; n < 40; n++) {
const flake = document.createElement('i');
const z = rand(-300, 150);
const near = (z + 300) / 450; // 0 = far … 1 = near
const t = (9 - near * 5) * rand(0.85, 1.15); // near flakes fall faster
flake.style.setProperty('--x', rand(-25, 125).toFixed(1) + '%');
flake.style.setProperty('--z', z.toFixed(0) + 'px');
flake.style.setProperty('--s', rand(4, 8).toFixed(1) + 'px');
flake.style.setProperty('--o', (0.45 + near * 0.5).toFixed(2));
flake.style.setProperty('--drift', rand(-40, 40).toFixed(0) + 'px');
flake.style.setProperty('--t', t.toFixed(2) + 's');
flake.style.setProperty('--d', -rand(0, t).toFixed(2) + 's'); // already falling
world.append(flake);
}
// JS writes each flake's values once (--x, --z, --s, --o, --drift, --t, --d); this one
// animation does all the falling. The fall is long in px because far flakes are scaled
// toward the centre by perspective and need a longer path to cross the whole frame.
$persp: 400px;
.d-snow {
position: absolute;
inset: 0;
overflow: hidden;
perspective: $persp;
background: linear-gradient(#050817, color-mix(in srgb, var(--accent) 32%, #0b1030) 75%, color-mix(in srgb, var(--accent-2) 16%, #121a3a));
&__moon {
position: absolute;
top: 12%;
right: 16%;
width: 38px;
height: 38px;
border-radius: 50%;
background: radial-gradient(circle at 60% 40%, #fffbe8, #e6e2ff 70%);
box-shadow:
0 0 24px rgb(255 250 230 / 0.55),
0 0 70px color-mix(in srgb, var(--accent) 45%, transparent);
}
// a snowy slope, flat, behind the 3D world
&__hill {
position: absolute;
left: -20%;
right: -20%;
bottom: -32%;
height: 62%;
border-radius: 50% 50% 0 0 / 34% 34% 0 0;
background: linear-gradient(#eef1ff, #b9c0e8 40%, #8d95c8);
box-shadow: 0 -6px 30px color-mix(in srgb, #dfe4ff 25%, transparent);
}
&__world {
position: absolute;
inset: 0;
transform-style: preserve-3d;
pointer-events: none;
// Pine trees at real depths: flakes further away pass behind them.
b {
position: absolute;
bottom: 3%;
left: var(--x);
width: 70px;
height: 104px;
background:
// snow on the tiers
linear-gradient(transparent 26%, #e9ecff 26% 30%, transparent 30% 55%, #e9ecff 55% 59%, transparent 59%),
linear-gradient(color-mix(in srgb, var(--accent-2) 30%, #0e2a2e), #07161c);
clip-path: polygon(
50% 0,
72% 28%,
62% 28%,
84% 57%,
70% 57%,
96% 88%,
56% 88%,
56% 100%,
44% 100%,
44% 88%,
4% 88%,
30% 57%,
16% 57%,
38% 28%,
28% 28%
);
transform: translateZ(var(--z));
}
i {
position: absolute;
top: 0;
left: var(--x);
width: var(--s);
height: var(--s);
border-radius: 50%;
background: radial-gradient(circle, #fff 0 35%, rgb(255 255 255 / 0) 70%);
opacity: var(--o);
animation: d-snow-fall var(--t) linear var(--d) infinite;
}
}
}
@keyframes d-snow-fall {
from {
transform: translate3d(0, -160px, var(--z));
}
to {
transform: translate3d(var(--drift), 560px, var(--z));
}
}