Pure CSS Scenes & objects #loop #sass-loop #generated

Warp starfield in pure CSS

Each layer is ONE element; its stars are box-shadows generated by Sass random().

  • 65 lines of CSS
  • 3 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

How it works

  1. Hundreds of DOM nodes would be wasteful. Instead each layer is one 1×1px element, and every star is a box-shadow of it.
  2. A box-shadow with zero blur and a spread radius is just a dot at an offset — and you can have as many as you like.
  3. Flying the single element along Z moves all its stars at once, with correct perspective.
  4. Three layers on staggered delays hide the loop. In Sass the list comes from random() at build time (see the SCSS tab); here it is written out.

Key techniques

  • box-shadow as particles
  • Sass random() at build time
  • translateZ fly-through

Copy-paste code

HTML 3 lines

<div class="space">
  <i></i><i></i><i></i>
</div>

CSS 65 lines

.space {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  perspective: 300px;
  background: radial-gradient(circle, #0d1030, #02030a 75%);
}

.space i {
  position: absolute;
  width: 1px;
  height: 1px;
  border-radius: 50%;
  animation: warp 3s linear infinite;
  box-shadow:
    -450px 379px 0 0.5px #fff, -261px 206px 0 1px #fff, -154px 227px 0 1px #fff, 39px 166px 0 1px #fff,
    285px -232px 0 0.5px #fff, 187px 178px 0 0.5px #fff, 166px -29px 0 1px #fff, 277px -339px 0 1px #fff,
    381px 170px 0 1px #fff, 70px 371px 0 1px #fff, -115px -148px 0 1px #fff, -151px -313px 0 0.5px #fff,
    -64px -185px 0 1px #fff, 52px 206px 0 1px #fff, -397px -204px 0 1px #fff, 8px -71px 0 0.5px #fff,
    368px -401px 0 0.5px #fff, -140px 148px 0 1px #fff, 13px -301px 0 0.5px #fff, -195px -122px 0 0.5px #fff,
    -151px 28px 0 1px #fff, -240px 366px 0 0.5px #fff, 327px 122px 0 1px #fff, 15px 4px 0 1px #fff,
    396px -93px 0 1px #fff, -224px 78px 0 0.5px #fff, -152px -98px 0 1px #fff, 340px -350px 0 1px #fff,
    -380px -84px 0 0.5px #fff, -380px 447px 0 0.5px #fff, 119px -320px 0 0.5px #fff, 84px -326px 0 1px #fff,
    346px 352px 0 1px #fff, 367px 235px 0 0.5px #fff, -441px -227px 0 0.5px #fff, -449px 105px 0 0.5px #fff,
    217px -10px 0 1px #fff, -236px 344px 0 0.5px #fff, -113px 355px 0 0.5px #fff, -10px -447px 0 1px #fff;
}

.space i:nth-child(2) {
  animation-delay: -1s;
  box-shadow:
    -449px -430px 0 1px #fff, -84px 220px 0 1px #fff, 142px -263px 0 0.5px #fff, 31px 420px 0 0.5px #fff,
    -211px -70px 0 0.5px #fff, -54px -51px 0 1px #fff, 412px 106px 0 0.5px #fff, 441px -172px 0 0.5px #fff,
    243px -301px 0 0.5px #fff, 214px -284px 0 0.5px #fff, -340px -38px 0 1px #fff, 179px 335px 0 0.5px #fff,
    118px 346px 0 0.5px #fff, -298px -39px 0 0.5px #fff, 39px 74px 0 1px #fff, -274px 29px 0 1px #fff,
    448px -266px 0 1px #fff, -184px 36px 0 0.5px #fff, 61px -14px 0 0.5px #fff, 331px -440px 0 1px #fff,
    55px 403px 0 0.5px #fff, 76px 160px 0 1px #fff, -137px 434px 0 1px #fff, -179px 63px 0 0.5px #fff,
    -62px -29px 0 0.5px #fff, 432px -309px 0 1px #fff, -99px -225px 0 1px #fff, -80px 64px 0 0.5px #fff,
    411px -158px 0 1px #fff, 284px 402px 0 1px #fff, -372px -157px 0 1px #fff, 283px 18px 0 1px #fff,
    266px -171px 0 1px #fff, -338px -276px 0 0.5px #fff, -59px 139px 0 0.5px #fff, 84px -56px 0 0.5px #fff,
    369px -135px 0 1px #fff, -377px 363px 0 0.5px #fff, 324px -124px 0 1px #fff, -394px 231px 0 1px #fff;
}

.space i:nth-child(3) {
  animation-delay: -2s;
  box-shadow:
    -436px -241px 0 0.5px #fff, -173px -161px 0 0.5px #fff, -254px -430px 0 1px #fff, 287px -263px 0 0.5px #fff,
    336px 427px 0 0.5px #fff, -348px -84px 0 1px #fff, -420px -190px 0 1px #fff, -425px -370px 0 0.5px #fff,
    128px 3px 0 0.5px #fff, 29px 434px 0 0.5px #fff, -10px 170px 0 0.5px #fff, -389px 297px 0 0.5px #fff,
    -36px 427px 0 1px #fff, -49px -444px 0 1px #fff, 148px 263px 0 1px #fff, -345px -64px 0 1px #fff,
    418px -183px 0 0.5px #fff, 386px -220px 0 0.5px #fff, -313px -440px 0 1px #fff, 321px -237px 0 1px #fff,
    -434px -311px 0 1px #fff, 200px 417px 0 1px #fff, 159px 129px 0 0.5px #fff, -209px -62px 0 1px #fff,
    282px 60px 0 0.5px #fff, -415px 39px 0 0.5px #fff, -176px 355px 0 1px #fff, 216px -345px 0 0.5px #fff,
    -151px -274px 0 0.5px #fff, -240px 362px 0 0.5px #fff, 438px 446px 0 0.5px #fff, 329px -77px 0 0.5px #fff,
    -404px -254px 0 0.5px #fff, 233px 3px 0 0.5px #fff, -164px 392px 0 0.5px #fff, -424px -388px 0 1px #fff,
    204px 381px 0 1px #fff, 40px -20px 0 1px #fff, 175px -191px 0 0.5px #fff, 98px 66px 0 1px #fff;
}

/* stop at 2/3 of the perspective distance; at z = perspective the scale is infinite */
@keyframes warp {
  from     { opacity: 0; transform: translateZ(-600px); }
  20%, 80% { opacity: 1; }
  to       { opacity: 0; transform: translateZ(200px); }
}

Sass source 65 lines

@use 'sass:list';
@use 'sass:math';

// $count random white dots inside a $size square, returned as one box-shadow list.
// random() runs at BUILD time: the browser just receives a long static value.
@function stars($count, $size) {
  $shadows: ();

  @for $i from 1 through $count {
    $x: math.random($size) - math.div($size, 2);
    $y: math.random($size) - math.div($size, 2);
    $shadows: list.append($shadows, #{$x}px #{$y}px 0 #{math.random(2) * 0.5}px #fff, comma);
  }

  @return $shadows;
}

.d-starfield {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  perspective: 300px;
  background: radial-gradient(circle, #0d1030, #02030a 75%);

  // One element per layer; every star is a shadow of this single 1px dot.
  i {
    position: absolute;
    width: 1px;
    height: 1px;
    border-radius: 50%;
    box-shadow: stars(70, 700);
    animation: d-starfield-warp 3s linear infinite;
  }

  i:nth-child(2) {
    box-shadow: stars(70, 700);
    animation-delay: -1s;
  }

  i:nth-child(3) {
    box-shadow: stars(70, 700);
    animation-delay: -2s;
  }
}

@keyframes d-starfield-warp {
  from {
    opacity: 0;
    transform: translateZ(-600px);
  }

  20%,
  80% {
    opacity: 1;
  }

  // Stop at 2/3 of the perspective distance (3x magnification) and fade out.
  // At z = perspective the scale would be infinite.
  to {
    opacity: 0;
    transform: translateZ(200px);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.