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 road plane laid flat with rotateX(90deg). The centre line slides by exactly one dash period, and posts travel the whole road with staggered delays, so the loop never shows a seam.
Your edited version
transform-origin: bottom; rotateX(90deg). Put perspective-origin on the horizon line, and the far end vanishes exactly there.rotateX(-90deg) around their foot and all run the same trip along the road, spread out by negative delays. Speed matches the dashes: 1400px in 7s = 100px in 0.5s.mask on the road would flatten its 3D posts. Instead a flat band of haze sits on top of the horizon and hides where things appear. The ground lines are a repeating-conic-gradient from the vanishing point.rotateX(90deg) floor + perspective-origin = horizontranslate by one pattern periodupright posts: rotateX(-90deg), origin bottomflat haze overlay instead of a mask<div class="drive">
<div class="sun"></div>
<div class="ground"></div>
<div class="road">
<div class="dashes"></div>
<i class="left" style="--i:0"></i>
<i class="right" style="--i:0"></i>
<i class="left" style="--i:1"></i>
<i class="right" style="--i:1"></i>
<i class="left" style="--i:2"></i>
<i class="right" style="--i:2"></i>
<i class="left" style="--i:3"></i>
<i class="right" style="--i:3"></i>
<i class="left" style="--i:4"></i>
<i class="right" style="--i:4"></i>
<i class="left" style="--i:5"></i>
<i class="right" style="--i:5"></i>
</div>
<div class="haze"></div>
</div>
.drive {
position: fixed;
inset: 0;
overflow: hidden;
perspective: 300px;
perspective-origin: 50% 44%; /* the horizon */
background: linear-gradient(#07051a, #3a2580 24%, #8c2a6c 44%);
}
.sun {
position: absolute;
top: calc(44% - 78px);
left: calc(50% - 60px);
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(#ffb547 20%, #ff4d9d 75%);
box-shadow: 0 0 40px rgb(255 77 157 / 0.6);
mask: linear-gradient(#000 30%, transparent 0 33%, #000 0 41%, transparent 0 45%, #000 0 52%, transparent 0 57%, #000 0);
}
/* lines from the vanishing point: parallel to the road, so they stay still */
.ground {
position: absolute;
inset: 44% 0 0;
background:
repeating-conic-gradient(from 90deg at 50% 0%, rgb(255 77 157 / 0.5) 0 0.5deg, transparent 0.5deg 9deg),
linear-gradient(#3b1a6e, #06030f 70%);
}
.road {
position: absolute;
left: 29%;
right: 29%;
bottom: 0;
height: 1400px;
background: linear-gradient(90deg, #ff4d9d 0 3px, #1c1438 3px calc(100% - 3px), #ff4d9d calc(100% - 3px));
transform-style: preserve-3d;
transform-origin: bottom;
/* the near end starts 120px in front of the screen (well below the 300px perspective) */
transform: translateZ(120px) rotateX(90deg);
}
.dashes {
position: absolute;
top: -100px; /* one period taller than the road */
bottom: 0;
left: calc(50% - 3px);
width: 6px;
background: repeating-linear-gradient(#ffb547 0 50px, transparent 50px 100px);
animation: dash 0.5s linear infinite;
}
.road i {
position: absolute;
bottom: 0;
width: 5px;
height: 46px;
border-radius: 3px;
background: linear-gradient(#2ee6d6, rgb(46 230 214 / 0.15));
box-shadow: 0 0 8px rgb(46 230 214 / 0.7);
transform-origin: bottom;
animation: post 7s linear infinite;
animation-delay: calc(var(--i) * -7s / 6);
}
.road i::before {
content: '';
position: absolute;
top: -4px;
left: -3px;
width: 11px;
height: 11px;
border-radius: 50%;
background: radial-gradient(circle, #fff 0 25%, #2ee6d6 60%);
}
.road .left {
left: -22px;
}
.road .right {
right: -22px;
animation-delay: calc(var(--i) * -7s / 6 - 7s / 12); /* half a step later */
}
.haze {
position: absolute;
inset: calc(44% - 7%) 0 auto;
height: 22%;
background: linear-gradient(transparent, #8c2a6c 30% 50%, transparent);
}
/* translateZ(1px): paint just above the road surface */
@keyframes dash {
from { transform: translateZ(1px) translateY(0); }
to { transform: translateZ(1px) translateY(100px); }
}
@keyframes post {
from { opacity: 0; transform: translateY(-1400px) rotateX(-90deg); }
12% { opacity: 1; }
to { opacity: 1; transform: translateY(0) rotateX(-90deg); }
}
@use 'sass:math';
// A synthwave drive. The road is one long plane laid flat with rotateX(90deg) around its
// bottom edge; perspective-origin sits on the horizon, so the far end vanishes exactly there.
// Nothing really travels: the centre line slides by ONE dash period and jumps back (seamless),
// and the posts all run the same trip, spread along the road by negative delays.
$horizon: 44%;
$persp: 300px;
$front: 120px; // the near end of the road, in front of the screen plane (< $persp)
$len: 1400px; // road length
$period: 100px; // one dash + one gap
$dash-time: 0.5s; // time to slide one period
$posts: 6; // per side
$post-time: $dash-time * math.div($len, $period); // same speed as the dashes
$step: math.div($post-time, $posts);
$sky-low: color-mix(in srgb, var(--hot) 50%, #2a0a3a);
.d-road {
position: absolute;
inset: 0;
overflow: hidden;
perspective: $persp;
perspective-origin: 50% $horizon;
background: linear-gradient(#07051a, color-mix(in srgb, var(--accent) 45%, #120733) 24%, $sky-low $horizon);
// striped sunset sun; the ground (next sibling) covers its lower half
&__sun {
position: absolute;
top: calc(#{$horizon} - 78px);
left: calc(50% - 60px);
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(var(--warm) 20%, var(--hot) 75%);
box-shadow: 0 0 40px color-mix(in srgb, var(--hot) 60%, transparent);
mask: linear-gradient(#000 30%, transparent 0 33%, #000 0 41%, transparent 0 45%, #000 0 52%, transparent 0 57%, #000 0);
}
// Flat ground with lines radiating from the vanishing point. They run parallel to the road,
// so they would not move while driving straight: static is correct.
&__ground {
position: absolute;
inset: $horizon 0 0;
background:
repeating-conic-gradient(from 90deg at 50% 0%, color-mix(in srgb, var(--hot) 50%, transparent) 0 0.5deg, transparent 0.5deg 9deg),
linear-gradient(color-mix(in srgb, var(--accent) 40%, #1a0830), #06030f 70%);
}
&__plane {
position: absolute;
left: 29%;
right: 29%;
bottom: 0;
height: $len;
background: linear-gradient(
90deg,
var(--hot) 0 3px,
color-mix(in srgb, var(--accent) 22%, #0a0616) 3px calc(100% - 3px),
var(--hot) calc(100% - 3px)
);
transform-style: preserve-3d;
transform-origin: bottom;
transform: translateZ($front) rotateX(90deg);
// Posts: stand up from the road (rotateX(-90deg) around their foot) and ride it from
// the far end to the near end. The 0 → 1 opacity hides the pop-in in the haze.
i {
position: absolute;
bottom: 0;
width: 5px;
height: 46px;
border-radius: 3px;
background: linear-gradient(var(--accent-2), color-mix(in srgb, var(--accent-2) 15%, transparent));
box-shadow: 0 0 8px color-mix(in srgb, var(--accent-2) 70%, transparent);
transform-origin: bottom;
animation: d-road-post $post-time linear infinite;
animation-delay: calc(var(--i) * #{-$step});
&::before {
content: '';
position: absolute;
top: -4px;
left: -3px;
width: 11px;
height: 11px;
border-radius: 50%;
background: radial-gradient(circle, #fff 0 25%, var(--accent-2) 60%);
}
}
.is-left {
left: -22px;
}
// half a step later than the left row, so the two sides alternate
.is-right {
right: -22px;
animation-delay: calc(var(--i) * #{-$step} - #{$step * 0.5});
}
}
// the centre line: taller than the road by one period, so the slide never shows its end
&__dashes {
position: absolute;
top: -$period;
bottom: 0;
left: calc(50% - 3px);
width: 6px;
background: repeating-linear-gradient(var(--warm) 0 50px, transparent 50px $period);
animation: d-road-dash $dash-time linear infinite;
}
// A flat band of haze over the horizon hides where the road and posts vanish.
// (A mask on the plane would flatten its 3D posts.)
&__haze {
position: absolute;
inset: calc(#{$horizon} - 7%) 0 auto;
height: 22%;
background: linear-gradient(transparent, $sky-low 30% 50%, transparent);
pointer-events: none;
}
}
// translateZ(1px) keeps the paint just above the road surface (no z-fighting)
@keyframes d-road-dash {
from {
transform: translateZ(1px) translateY(0);
}
to {
transform: translateZ(1px) translateY($period);
}
}
@keyframes d-road-post {
from {
opacity: 0;
transform: translateY(-$len) rotateX(-90deg);
}
12% {
opacity: 1;
}
to {
opacity: 1;
transform: translateY(0) rotateX(-90deg);
}
}