Pure CSS Scenes & objects #loop

Infinite tunnel in pure CSS

Identical frames fly from far away to past the camera on staggered negative delays.

  • 30 lines of CSS
  • 12 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. Ten identical frames, stacked in the centre, all running the same animation: from 1400 units behind the screen (translateZ −1400) to 200 units in front of it.
  2. Perspective is 320 units. A frame at z = 200 is already magnified 2.7× and off-screen, so stop there and fade out. Never animate all the way to z = perspective: the scale there is infinite and the browser stalls rasterising it.
  3. A negative animation-delay of i × (duration / count) spreads them evenly along the tunnel.
  4. Fading in from 0 opacity hides the moment a frame pops into existence far away.
  5. The tunnel is inset: 0, so it fills the canvas edge to edge. Its lengths are multiples of one base unit, --u, tied to the canvas, so a frame is the same share of a gallery card and of a full screen.

Key techniques

  • animated translateZ
  • negative animation-delay
  • opacity fade-in at distance

Copy-paste code

HTML 12 lines

<div class="tunnel">
  <i style="--i:0"></i>
  <i style="--i:1"></i>
  <i style="--i:2"></i>
  <i style="--i:3"></i>
  <i style="--i:4"></i>
  <i style="--i:5"></i>
  <i style="--i:6"></i>
  <i style="--i:7"></i>
  <i style="--i:8"></i>
  <i style="--i:9"></i>
</div>

CSS 30 lines

.tunnel {
  /* one base unit, tied to the canvas; the tunnel itself fills the canvas edge to edge */
  --u: 0.33vmin;
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  perspective: calc(320 * var(--u));
  background: radial-gradient(circle, #1b0f3a, #05030d 70%);
}

.tunnel i {
  box-sizing: content-box; /* a frame is its opening, its border drawn outside it: by design, on any page */
  position: absolute;
  width: calc(260 * var(--u));
  height: calc(260 * var(--u));
  border: calc(3 * var(--u)) solid hsl(calc(260 + var(--i) * 14) 90% 65%);
  border-radius: calc(18 * var(--u));
  box-shadow: 0 0 calc(18 * var(--u)) hsl(calc(260 + var(--i) * 14) 90% 65% / 0.7);
  animation: fly 4s linear infinite;
  animation-delay: calc(var(--i) * -0.4s);   /* 4s / 10 frames */
}

/* stop well before z = perspective (320 units): the scale there is infinite */
@keyframes fly {
  from     { opacity: 0; transform: translateZ(calc(-1400 * var(--u))) rotateZ(0deg); }
  25%, 85% { opacity: 1; }
  to       { opacity: 0; transform: translateZ(calc(200 * var(--u))) rotateZ(90deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.