Pure CSS Shapes & solids #loop #shape #grid

Breathing lattice in pure CSS

A 3×3×3 wireframe: three layers of bars and glowing nodes, and nine posts through them. The outer layers move apart while the posts stretch by the same amount, so no joint ever opens.

  • 92 lines of CSS
  • 66 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

How it works

  1. A single flat bar vanishes whenever you see it edge-on, so every bar is two crossed planes: the element and its ::after turned rotateX(90deg) around the bar's own length.
  2. One bar rule, three directions: along X as it is, along Z with rotateY(90deg), standing up with rotateZ(90deg). --x, --y, --z (−1, 0 or 1) times the spacing place it.
  3. The lattice is three horizontal layers (6 bars and 9 nodes each) plus 9 posts running through all of them. A layer moves to translateY(y × 1.3 × u): the middle one has y = 0, so the same keyframes leave it in place.
  4. The posts wrapper does scaleY(1.3) on the same beat, so the posts stretch by exactly what the layers move and no joint ever opens.
  5. Nodes are round glows that turn back against the spin (rotateY(-360deg), same timing), so they always face you.

Key techniques

  • bars as two crossed planes
  • layers: translateY(y × distance)
  • posts: scaleY in step with the layers
  • billboard nodes

Copy-paste code

HTML 66 lines

<div class="scene">
  <div class="lattice">
    <div class="layer" style="--y:-1">
      <i style="--z:-1"></i>
      <i style="--z:0"></i>
      <i style="--z:1"></i>
      <i class="z" style="--x:-1"></i>
      <i class="z" style="--x:0"></i>
      <i class="z" style="--x:1"></i>
      <b style="--x:-1; --z:-1"></b>
      <b style="--x:0; --z:-1"></b>
      <b style="--x:1; --z:-1"></b>
      <b style="--x:-1; --z:0"></b>
      <b style="--x:0; --z:0"></b>
      <b style="--x:1; --z:0"></b>
      <b style="--x:-1; --z:1"></b>
      <b style="--x:0; --z:1"></b>
      <b style="--x:1; --z:1"></b>
    </div>
    <div class="layer mid" style="--y:0">
      <i style="--z:-1"></i>
      <i style="--z:0"></i>
      <i style="--z:1"></i>
      <i class="z" style="--x:-1"></i>
      <i class="z" style="--x:0"></i>
      <i class="z" style="--x:1"></i>
      <b style="--x:-1; --z:-1"></b>
      <b style="--x:0; --z:-1"></b>
      <b style="--x:1; --z:-1"></b>
      <b style="--x:-1; --z:0"></b>
      <b style="--x:0; --z:0"></b>
      <b style="--x:1; --z:0"></b>
      <b style="--x:-1; --z:1"></b>
      <b style="--x:0; --z:1"></b>
      <b style="--x:1; --z:1"></b>
    </div>
    <div class="layer" style="--y:1">
      <i style="--z:-1"></i>
      <i style="--z:0"></i>
      <i style="--z:1"></i>
      <i class="z" style="--x:-1"></i>
      <i class="z" style="--x:0"></i>
      <i class="z" style="--x:1"></i>
      <b style="--x:-1; --z:-1"></b>
      <b style="--x:0; --z:-1"></b>
      <b style="--x:1; --z:-1"></b>
      <b style="--x:-1; --z:0"></b>
      <b style="--x:0; --z:0"></b>
      <b style="--x:1; --z:0"></b>
      <b style="--x:-1; --z:1"></b>
      <b style="--x:0; --z:1"></b>
      <b style="--x:1; --z:1"></b>
    </div>
    <div class="posts">
      <i style="--x:-1; --z:-1"></i>
      <i style="--x:0; --z:-1"></i>
      <i style="--x:1; --z:-1"></i>
      <i style="--x:-1; --z:0"></i>
      <i style="--x:0; --z:0"></i>
      <i style="--x:1; --z:0"></i>
      <i style="--x:-1; --z:1"></i>
      <i style="--x:0; --z:1"></i>
      <i style="--x:1; --z:1"></i>
    </div>
  </div>
</div>

CSS 92 lines

.scene {
  perspective: 800px;
}

.lattice {
  --u: 50px; /* spacing */
  position: relative;
  width: 0;
  height: 0;
  transform-style: preserve-3d;
  animation: spin 18s linear infinite;
}

.layer,
.posts {
  position: absolute;
  transform-style: preserve-3d;
}

.layer {
  transform: translateY(calc(var(--y) * var(--u)));
  animation: breathe 2.4s ease-in-out infinite alternate;
}

.posts {
  animation: stretch 2.4s ease-in-out infinite alternate;
}

/* a bar along X, two crossed planes */
.lattice i {
  position: absolute;
  top: -1px;
  left: calc(var(--u) * -1);
  width: calc(var(--u) * 2);
  height: 2px;
  transform-style: preserve-3d;
  background: linear-gradient(90deg, #2ee6d6, rgb(139 108 255 / 0.85) 30% 70%, #2ee6d6);
  transform: translateZ(calc(var(--z) * var(--u)));
}

.lattice i::after {
  content: '';
  position: absolute;
  inset: 0;
  background: inherit;
  transform: rotateX(90deg);
}

/* the same bar along Z */
.lattice i.z {
  transform: translateX(calc(var(--x) * var(--u))) rotateY(90deg);
}

/* and standing up */
.posts i {
  transform: translateX(calc(var(--x) * var(--u))) translateZ(calc(var(--z) * var(--u))) rotateZ(90deg);
}

.lattice b {
  --glow: 46 230 214;
  position: absolute;
  top: -6px;
  left: -6px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, rgb(var(--glow)) 25%, #fff) 0 22%, rgb(var(--glow)) 42%, rgb(var(--glow) / 0) 72%);
  animation: face-you 18s linear infinite;
}

.mid b { --glow: 255 77 157; }
.mid b:nth-of-type(5) { --glow: 255 181 71; } /* the very centre */

@keyframes spin {
  from { transform: rotateZ(-8deg) rotateX(-22deg) rotateY(0deg); }
  to   { transform: rotateZ(-8deg) rotateX(-22deg) rotateY(360deg); }
}

/* y = 0 for the middle layer, so it stays put */
@keyframes breathe {
  to { transform: translateY(calc(var(--y) * var(--u) * 1.3)); }
}

/* (u + 0.3u) / u: exactly what the outer layers moved */
@keyframes stretch {
  to { transform: scaleY(1.3); }
}

@keyframes face-you {
  from { transform: translate3d(calc(var(--x) * var(--u)), 0, calc(var(--z) * var(--u))) rotateY(0deg); }
  to   { transform: translate3d(calc(var(--x) * var(--u)), 0, calc(var(--z) * var(--u))) rotateY(-360deg); }
}

Sass source 123 lines

$u: 50px; // lattice spacing
$p: 0.3; // how far the outer layers breathe out, as a share of $u
$spin: 18s;
$beat: 2.4s;

// A 3 × 3 × 3 lattice as three horizontal layers (each: 6 bars and 9 nodes) plus 9 posts running
// through all three. The outer layers move out along Y and the posts stretch with them (scaleY),
// so every joint stays joined.
.d-lattice {
  position: relative;
  width: 0;
  height: 0;
  transform-style: preserve-3d;
  animation: d-lattice-spin $spin linear infinite;

  // Everything is placed from the lattice's centre. --x / --y / --z are -1, 0 or 1.
  &__layer,
  &__posts {
    position: absolute;
    transform-style: preserve-3d;
  }

  &__layer {
    transform: translateY(calc(var(--y) * #{$u}));
    animation: d-lattice-out $beat ease-in-out infinite alternate;
  }

  // the posts stretch by exactly what the layers move: (u + p·u) / u
  &__posts {
    animation: d-lattice-stretch $beat ease-in-out infinite alternate;
  }

  // A bar is two crossed planes (the element and its ::after): a single plane would vanish
  // whenever it is seen edge-on.
  i {
    position: absolute;
    top: -1px;
    left: -$u;
    width: $u * 2;
    height: 2px;
    transform-style: preserve-3d;
    background: linear-gradient(
      90deg,
      var(--accent-2),
      color-mix(in srgb, var(--accent) 85%, transparent) 30% 70%,
      var(--accent-2)
    );
    transform: translateZ(calc(var(--z) * #{$u})); // a bar along X, at depth z

    &::after {
      content: '';
      position: absolute;
      inset: 0;
      background: inherit;
      transform: rotateX(90deg);
    }

    // a bar along Z: the same bar turned a quarter, at x
    &.is-z {
      transform: translateX(calc(var(--x) * #{$u})) rotateY(90deg);
    }
  }

  // posts: the same bar standing up (rotateZ), at x and z
  &__posts i {
    transform: translateX(calc(var(--x) * #{$u})) translateZ(calc(var(--z) * #{$u})) rotateZ(90deg);
  }

  // nodes: glowing dots that turn back against the spin, so they always face you and stay round
  b {
    --glow: var(--accent-2);
    position: absolute;
    top: -6px;
    left: -6px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: radial-gradient(circle, color-mix(in srgb, var(--glow) 25%, #fff) 0 22%, var(--glow) 42%, color-mix(in srgb, var(--glow) 0%, transparent) 72%);
    animation: d-lattice-face $spin linear infinite;
  }

  // the middle layer glows pink, its very centre amber
  .is-mid b {
    --glow: var(--hot);

    &:nth-of-type(5) {
      --glow: var(--warm);
    }
  }
}

@keyframes d-lattice-spin {
  from {
    transform: rotateZ(-8deg) rotateX(-22deg) rotateY(0deg);
  }

  to {
    transform: rotateZ(-8deg) rotateX(-22deg) rotateY(360deg);
  }
}

// --y is 0 for the middle layer, so it stays put while the outer two move out
@keyframes d-lattice-out {
  to {
    transform: translateY(calc(var(--y) * #{$u * (1 + $p)}));
  }
}

@keyframes d-lattice-stretch {
  to {
    transform: scaleY(1 + $p);
  }
}

@keyframes d-lattice-face {
  from {
    transform: translate3d(calc(var(--x) * #{$u}), 0, calc(var(--z) * #{$u})) rotateY(0deg);
  }

  to {
    transform: translate3d(calc(var(--x) * #{$u}), 0, calc(var(--z) * #{$u})) rotateY(-360deg);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.