CSS + JS Loaders & patterns #generated #loop

Wave grid in CSS + JavaScript

JS builds the grid and gives each cell its distance from the centre; CSS turns that into a ripple.

  • 34 lines of CSS
  • 3 lines of HTML
  • 13 lines of JS
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. JS builds an n × n grid and, for each cell, stores its distance from the centre in --d.
  2. All cells run the same bobbing animation on translateZ.
  3. animation-delay: calc(var(--d) * -0.22s) offsets each cell by its distance, so the motion reads as a ripple spreading outward.
  4. Negative delays mean the wave is already in full swing on the first frame. A further 0.6s picks which moment that is: one with the wave spread evenly over the grid, since the first frame is what a paused card shows.
  5. The grid is 11 × 11 cells in a square 260 units of one base unit, --u, across, and the cells rise 40 of them. Tied to the canvas, that keeps the wave the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • generated DOM
  • animation-delay from distance
  • one shared @keyframes

Copy-paste code

HTML 3 lines

<div class="scene">
  <div class="grid"></div>
</div>

CSS 34 lines

.scene {
  /* one base unit: every length below is a multiple of it, so the wave is the same share of a
     gallery card, the editor, a full screen and a recording canvas */
  --u: 0.24vmin;
  perspective: calc(800 * var(--u));
}

.grid {
  display: grid;
  grid-template-columns: repeat(var(--n), 1fr);
  gap: calc(4 * var(--u));
  width: calc(260 * var(--u));
  height: calc(260 * var(--u));
  transform-style: preserve-3d;
  /* the solid part of the wave is the cells high up and near the camera, so what reads as the
     wave rides up and down the grid as the ripple spreads: the lift puts the middle of that ride,
     not the flat grid, in the middle of the canvas */
  transform: translateY(calc(-2 * var(--u))) rotateX(58deg) rotateZ(-45deg);
}

.grid i {
  border-radius: calc(3 * var(--u));
  background: #ffb547;
  animation: bob 2.4s ease-in-out infinite;
  /* the ripple, plus 0.6s into the loop: the first frame, which a paused card shows, has the
     middle up and the wave spread evenly over the grid */
  animation-delay: calc(var(--d) * -0.22s - 0.6s);
}

/* opacity + transform only: both run on the compositor, even with 100+ cells */
@keyframes bob {
  0%, 100% { transform: translateZ(calc(-14 * var(--u))); opacity: 0.4; }
  50%      { transform: translateZ(calc(40 * var(--u)));  opacity: 1; }
}

JS 13 lines

const grid = document.querySelector('.grid');
const N = 11;
const mid = (N - 1) / 2;

grid.style.setProperty('--n', N);

for (let row = 0; row < N; row++) {
  for (let col = 0; col < N; col++) {
    const cell = document.createElement('i');
    cell.style.setProperty('--d', Math.hypot(row - mid, col - mid).toFixed(2));
    grid.append(cell);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.