Pure CSS Shapes & solids #loop #shape #sass-loop #grid

Cube field wave in pure CSS

Sixteen cubes seen isometrically, rising and falling in a wave that travels along the diagonals. Each cube is one element: its top plus two folded pseudo-element sides.

  • 57 lines of CSS
  • 20 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed
GitHub

How it works

  1. Isometric view: turn a flat grid rotateZ(-45deg), then tip it back with rotateX. "Up" is now the grid's own Z axis, so translateZ lifts a cube straight off the floor.
  2. From this angle only the top and two sides of a cube can ever be seen. So each cube is one element: the element is the top, and ::before / ::after are the sides, hinged on its edges and folded down.
  3. Every cube runs the same up-and-down animation. --d is row + column, so cubes on the same diagonal share a delay and the wave travels corner to corner.
  4. The cube never leaves its grid cell and only transform animates, so 16 cubes stay cheap.

Key techniques

  • rotateX + rotateZ isometric floor
  • only the two visible sides, as ::before / ::after
  • translateZ along the floor normal
  • delay = (row + column) × step

Copy-paste code

HTML 20 lines

<div class="scene">
  <div class="field">
    <i style="--d:0"></i>
    <i style="--d:1"></i>
    <i style="--d:2"></i>
    <i style="--d:3"></i>
    <i style="--d:1"></i>
    <i style="--d:2"></i>
    <i style="--d:3"></i>
    <i style="--d:4"></i>
    <i style="--d:2"></i>
    <i style="--d:3"></i>
    <i style="--d:4"></i>
    <i style="--d:5"></i>
    <i style="--d:3"></i>
    <i style="--d:4"></i>
    <i style="--d:5"></i>
    <i style="--d:6"></i>
  </div>
</div>

CSS 57 lines

.scene {
  perspective: 800px;
}

.field {
  display: grid;
  grid-template-columns: repeat(4, 36px);
  gap: 10px;
  transform-style: preserve-3d;
  /* isometric floor: turn 45deg, then tip back */
  transform: translateY(18px) rotateX(58deg) rotateZ(-45deg);
}

/* the element itself is the top of the cube */
.field i {
  --hue: calc(253 - var(--d) * 13); /* violet corner to teal corner */
  position: relative;
  width: 36px;
  height: 36px;
  background: hsl(var(--hue) 90% 80%);
  transform-style: preserve-3d;
  animation: wave 2.6s ease-in-out infinite;
  animation-delay: calc(var(--d) * -0.28s);
}

.field i::before,
.field i::after {
  content: '';
  position: absolute;
}

/* side hinged on the bottom edge, folded down */
.field i::before {
  left: 0;
  top: 100%;
  width: 100%;
  height: 36px;
  background: hsl(var(--hue) 85% 64%);
  transform-origin: top;
  transform: rotateX(-90deg);
}

/* side hinged on the left edge, folded down */
.field i::after {
  right: 100%;
  top: 0;
  width: 36px;
  height: 100%;
  background: hsl(var(--hue) 55% 36%);
  transform-origin: right;
  transform: rotateY(-90deg);
}

@keyframes wave {
  0%, 100% { transform: translateZ(0); }
  50%      { transform: translateZ(44px); }
}

Sass source 78 lines

@use 'sass:math';

$n: 4;
$cell: 28px;
$gap: 8px;
$lift: 34px;

.d-cubegrid {
  display: grid;
  grid-template-columns: repeat($n, $cell);
  gap: $gap;
  transform-style: preserve-3d;
  // isometric view: turn the floor 45deg, then tip it back. "Up" is now the floor's own Z axis.
  transform: translateY(14px) rotateX(58deg) rotateZ(-45deg);

  // The <i> itself is the cube's top. The two sides that can ever be seen from this angle are
  // pseudo-elements hinged on its edges and folded down, so a whole cube is a single element.
  i {
    --c: color-mix(in srgb, var(--accent-2) var(--mix), var(--accent));
    position: relative;
    width: $cell;
    height: $cell;
    background: color-mix(in srgb, var(--c) 78%, white);
    transform-style: preserve-3d;
    // translateZ only: the cube never leaves its grid cell, it just travels along the floor's normal
    animation: d-cubegrid-wave 2.6s ease-in-out infinite;
    animation-delay: calc(var(--d) * -0.28s);

    &::before,
    &::after {
      content: '';
      position: absolute;
    }

    // side facing down-right on screen (hinged on the bottom edge)
    &::before {
      left: 0;
      top: 100%;
      width: 100%;
      height: $cell;
      background: var(--c);
      transform-origin: top;
      transform: rotateX(-90deg);
    }

    // side facing down-left on screen (hinged on the left edge)
    &::after {
      right: 100%;
      top: 0;
      width: $cell;
      height: 100%;
      background: color-mix(in srgb, var(--c) 58%, black);
      transform-origin: right;
      transform: rotateY(-90deg);
    }
  }

  // --d = column + row, so cubes on the same diagonal move together and the wave travels
  @for $y from 0 to $n {
    @for $x from 0 to $n {
      i:nth-child(#{$y * $n + $x + 1}) {
        --d: #{$x + $y};
        --mix: #{math.round(math.div($x + $y, ($n - 1) * 2) * 100) * 1%};
      }
    }
  }
}

@keyframes d-cubegrid-wave {
  0%,
  100% {
    transform: translateZ(0);
  }

  50% {
    transform: translateZ($lift);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.