Pure CSS Scenes & objects #loop #scene #road #synthwave

Endless drive in pure CSS

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.

  • 107 lines of CSS
  • 20 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. The road is one long plane (1400 units) folded flat with transform-origin: bottom; rotateX(90deg). Put perspective-origin on the horizon line, and the far end vanishes exactly there.
  2. Nothing really moves forward. The centre line is one period (100 units) taller than the road and slides down by exactly that period, then jumps back: the jump is invisible because the pattern looks identical.
  3. Posts stand up with rotateX(-90deg) around their foot and all run the same trip along the road, spread out by negative delays. Speed matches the dashes: 1400 units in 7s = 100 in 0.5s.
  4. A 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.
  5. The scene is 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, so the road reads the same on a gallery card and on a full screen.

Key techniques

  • rotateX(90deg) floor + perspective-origin = horizon
  • translate by one pattern period
  • upright posts: rotateX(-90deg), origin bottom
  • flat haze overlay instead of a mask

Copy-paste code

HTML 20 lines

<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>

CSS 107 lines

.drive {
  /* 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;
  perspective: calc(300 * var(--u));
  perspective-origin: 50% 44%; /* the horizon */
  background: linear-gradient(#07051a, #3a2580 24%, #8c2a6c 44%);
}

.sun {
  position: absolute;
  top: calc(44% - calc(78 * var(--u)));
  left: calc(50% - calc(60 * var(--u)));
  width: calc(120 * var(--u));
  height: calc(120 * var(--u));
  border-radius: 50%;
  background: linear-gradient(#ffb547 20%, #ff4d9d 75%);
  box-shadow: 0 0 calc(40 * var(--u)) 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: calc(1400 * var(--u));
  background: linear-gradient(90deg, #ff4d9d 0 calc(3 * var(--u)), #1c1438 calc(3 * var(--u)) calc(100% - calc(3 * var(--u))), #ff4d9d calc(100% - calc(3 * var(--u))));
  transform-style: preserve-3d;
  transform-origin: bottom;
  /* the near end starts 120 units in front of the screen (well below the 300-unit perspective) */
  transform: translateZ(calc(120 * var(--u))) rotateX(90deg);
}

.dashes {
  position: absolute;
  top: calc(-100 * var(--u)); /* one period taller than the road */
  bottom: 0;
  left: calc(50% - calc(3 * var(--u)));
  width: calc(6 * var(--u));
  background: repeating-linear-gradient(#ffb547 0 calc(50 * var(--u)), transparent calc(50 * var(--u)) calc(100 * var(--u)));
  animation: dash 0.5s linear infinite;
}

.road i {
  position: absolute;
  bottom: 0;
  width: calc(5 * var(--u));
  height: calc(46 * var(--u));
  border-radius: calc(3 * var(--u));
  background: linear-gradient(#2ee6d6, rgb(46 230 214 / 0.15));
  box-shadow: 0 0 calc(8 * var(--u)) 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: calc(-4 * var(--u));
  left: calc(-3 * var(--u));
  width: calc(11 * var(--u));
  height: calc(11 * var(--u));
  border-radius: 50%;
  background: radial-gradient(circle, #fff 0 25%, #2ee6d6 60%);
}

.road .left {
  left: calc(-22 * var(--u));
}

.road .right {
  right: calc(-22 * var(--u));
  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);
}

/* one unit of translateZ: paint just above the road surface */
@keyframes dash {
  from { transform: translateZ(calc(1 * var(--u))) translateY(0); }
  to   { transform: translateZ(calc(1 * var(--u))) translateY(calc(100 * var(--u))); }
}

@keyframes post {
  from { opacity: 0; transform: translateY(calc(-1400 * var(--u))) rotateX(-90deg); }
  12%  { opacity: 1; }
  to   { opacity: 1; transform: translateY(0) rotateX(-90deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.