CSS + JS Loaders & patterns #generated #controls #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.

  • 26 lines of CSS
  • 3 lines of HTML
  • 13 lines of JS
  • No dependencies
  • MIT licensed
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.

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

.scene {
  perspective: 800px;
}

.grid {
  display: grid;
  grid-template-columns: repeat(var(--n), 1fr);
  gap: 4px;
  width: 260px;
  height: 260px;
  transform-style: preserve-3d;
  transform: rotateX(58deg) rotateZ(-45deg);
}

.grid i {
  border-radius: 3px;
  background: #ffb547;
  animation: bob 2.4s ease-in-out infinite;
  animation-delay: calc(var(--d) * -0.22s);
}

/* opacity + transform only: both run on the compositor, even with 100+ cells */
@keyframes bob {
  0%, 100% { transform: translateZ(-14px); opacity: 0.4; }
  50%      { transform: translateZ(40px);  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);
  }
}

Sass source 64 lines

.d-wavegrid {
  display: grid;
  grid-template-rows: 1fr auto;
  width: 100%;
  height: 100%;
  padding: 8px 14px 10px;

  &__view {
    display: grid;
    place-items: center;
    perspective: 700px;
  }

  &__grid {
    // --n (columns) is set by JS together with the cells
    display: grid;
    grid-template-columns: repeat(var(--n), 1fr);
    gap: 3px;
    width: 170px;
    height: 170px;
    transform-style: preserve-3d;
    transform: rotateX(58deg) rotateZ(-45deg);

    i {
      border-radius: 3px;
      background: var(--warm);
      animation: d-wavegrid-bob 2.4s ease-in-out infinite;
      // --d = distance from the centre, so the ripple travels outward
      animation-delay: calc(var(--d) * -0.22s);
    }
  }

  label {
    display: flex;
    align-items: center;
    gap: 8px;
    color: var(--muted);
    font-family: var(--mono);
    font-size: 11px;
  }

  output {
    min-width: 5ch;
    color: var(--text);
  }

  input {
    flex: 1;
    accent-color: var(--warm);
  }
}

@keyframes d-wavegrid-bob {
  0%,
  100% {
    opacity: 0.4;
    transform: translateZ(-14px);
  }

  50% {
    opacity: 1;
    transform: translateZ(34px);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.