Pure CSS Scenes & objects #loop #generated

Warp starfield in pure CSS

Each layer is ONE element; its stars are box-shadows of it, written out as one list.

  • 68 lines of CSS
  • 3 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. Hundreds of DOM nodes would be wasteful. Instead each layer is one element a single unit square, 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. The star lists are written out, so the field is the same on every load.
  5. The field is inset: 0, so it fills the canvas edge to edge. Its lengths are multiples of one base unit, --u, tied to the canvas. Each layer sets font-size: var(--u), so its star offsets are written in em: one em is one unit, and the list stays short enough to read.

Key techniques

  • box-shadow as particles
  • star offsets in em: one em = one unit
  • translateZ fly-through

Copy-paste code

HTML 3 lines

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

CSS 68 lines

.space {
  /* one base unit, tied to the canvas; the field itself fills the canvas edge to edge */
  --u: 0.33vmin;
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  perspective: calc(300 * var(--u));
  background: radial-gradient(circle, #0d1030, #02030a 75%);
}

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

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

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

/* stop at 2/3 of the perspective distance; at z = perspective the scale is infinite */
@keyframes warp {
  from     { opacity: 0; transform: translateZ(calc(-600 * var(--u))); }
  20%, 80% { opacity: 1; }
  to       { opacity: 0; transform: translateZ(calc(200 * var(--u))); }
}
Standalone snippet · plain CSS, no build step, no dependencies.