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.--z: from 53% of the canvas above the top to 185% below it, drifting sideways by --drift. The night is a size container, so those are cqh and cqw, shares of the canvas. Perspective makes near flakes big and fast, far ones small and slow.inset: 0 and laid out in percentages, so it fills the canvas edge to edge whatever its shape. Its lengths are multiples of one base unit, --u, tied to the canvas, and JS writes depth and size as plain numbers that CSS multiplies by it, so a flake is the same size on a gallery card and on a full screen. The drift is a plain number too, which CSS counts in shares of the canvas width.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:-240"></b>
<b style="--x:66%;--z:-140"></b>
<b style="--x:34%;--z:-20"></b>
</div>
</div>
.night {
/* one base unit, tied to the canvas; the scene itself fills the canvas edge to edge, its
layout in percentages of it and its lengths in units */
--u: 0.33vmin;
position: fixed;
inset: 0;
overflow: hidden;
container-type: size; /* inside it, 1cqw and 1cqh are 1% of the canvas across and down */
perspective: calc(400 * var(--u));
background: linear-gradient(#050817, #1f1d5a 75%, #1a2e4a);
}
.moon {
position: absolute;
top: 12%;
right: 16%;
width: calc(38 * var(--u));
height: calc(38 * var(--u));
border-radius: 50%;
background: radial-gradient(circle at 60% 40%, #fffbe8, #e6e2ff 70%);
box-shadow: 0 0 calc(24 * var(--u)) rgb(255 250 230 / 0.55), 0 0 calc(70 * var(--u)) 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; --z, --s and --drift are plain numbers, counted in units */
.world b {
position: absolute;
bottom: 3%;
left: var(--x);
width: calc(70 * var(--u));
height: calc(104 * var(--u));
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(calc(var(--z) * var(--u)));
}
.world i {
position: absolute;
top: 0;
left: var(--x);
width: calc(var(--s) * var(--u));
height: calc(var(--s) * var(--u));
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;
}
/* the fall and the drift are shares of the canvas (cqh, cqw), so on any canvas shape a flake is at
the same share of it at the same moment; only its depth and size are in units */
@keyframes fall {
from { transform: translate3d(0, -53cqh, calc(var(--z) * var(--u))); }
to { transform: translate3d(calc(var(--drift) * 0.33cqw), 185cqh, calc(var(--z) * var(--u))); }
}
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) + '%');
// lengths go in as plain numbers: the CSS counts depth and size in --u, and the drift in shares of the canvas
flake.style.setProperty('--z', z.toFixed(0));
flake.style.setProperty('--s', rand(4, 8).toFixed(1));
flake.style.setProperty('--o', (0.45 + near * 0.5).toFixed(2));
flake.style.setProperty('--drift', rand(-40, 40).toFixed(0));
flake.style.setProperty('--t', t.toFixed(2) + 's');
flake.style.setProperty('--d', -rand(0, t).toFixed(2) + 's'); // already falling
world.append(flake);
}