Pure CSS Scenes & objects #loop

Infinite tunnel in pure CSS

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

  • 27 lines of CSS
  • 12 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

How it works

  1. Ten identical frames, stacked in the centre, all running the same animation: translateZ(-1400px)translateZ(320px).
  2. Perspective is 320px. A frame at z = 200px 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.

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

.tunnel {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  perspective: 320px;
  background: radial-gradient(circle, #1b0f3a, #05030d 70%);
}

.tunnel i {
  position: absolute;
  width: 260px;
  height: 260px;
  border: 3px solid hsl(calc(260 + var(--i) * 14) 90% 65%);
  border-radius: 18px;
  box-shadow: 0 0 18px 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 (320px): the scale there is infinite */
@keyframes fly {
  from     { opacity: 0; transform: translateZ(-1400px) rotateZ(0deg); }
  25%, 85% { opacity: 1; }
  to       { opacity: 0; transform: translateZ(200px) rotateZ(90deg); }
}

Sass source 45 lines

@use 'sass:math';

$frames: 10;
$duration: 4s;

.d-tunnel {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  perspective: 320px;
  background: radial-gradient(circle, #1b0f3a, #05030d 70%);

  i {
    position: absolute;
    width: 200px;
    height: 200px;
    border: 3px solid hsl(calc(260 + var(--i) * 14) 90% 65%);
    border-radius: 18px;
    box-shadow: 0 0 18px hsl(calc(260 + var(--i) * 14) 90% 65% / 0.7);
    animation: d-tunnel-fly $duration linear infinite;
    // Spread the frames evenly across one loop. --i is 0…9 from the markup.
    animation-delay: calc(var(--i) * #{math.div(-$duration, $frames)});
  }
}

@keyframes d-tunnel-fly {
  from {
    opacity: 0;
    transform: translateZ(-1400px) rotateZ(0deg);
  }

  25%,
  85% {
    opacity: 1;
  }

  // Perspective is 320px, so z = 200px already magnifies 2.7x (the frame is off-screen).
  // Never animate up to z = perspective: the scale there is infinite.
  to {
    opacity: 0;
    transform: translateZ(200px) rotateZ(90deg);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.