Pure CSS Scenes & objects #loop #isometric #scene #voxel

Floating island in pure CSS

A small terrain of block columns in an isometric view. Each block is one element: its top is lifted with translateZ and two pseudo-element walls hang from its edges.

  • 103 lines of CSS
  • 30 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. The land is a 4 × 4 grid tilted to an isometric view with rotateX(58deg) and a rotateZ that sways between 38° and 52°. Each block gets --x/--y (cells), --z (top height) and --h (wall length).
  2. A block is one element: the element is the top face, lifted with translateZ. ::before folds down from its bottom edge with rotateX(-90deg), ::after from its right edge with rotateY(90deg). Only the two walls that face the camera exist.
  3. The same block makes everything: grass columns, the stepped rock underside (tops at z ≤ 0, walls hanging below), tree trunks and blossoms. Only the colours change.
  4. Floating is two animations with the same timing: the land bobs and sways, while a flat glow underneath shrinks and fades when the land is high.
  5. Every length is a multiple of one base unit, --u, tied to the canvas: a cell is 28 units and one step of height 14, so the island is the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • one element per block
  • walls: rotateX(-90deg) / rotateY(90deg) from the top edges
  • a height map in --x / --y / --z per block
  • bobbing + breathing shadow

Copy-paste code

HTML 30 lines

<div class="scene">
  <div class="island">
    <div class="shadow"></div>
    <div class="land">
      <i style="--x:0;--y:0;--z:2"></i>
      <i style="--x:1;--y:0;--z:3"></i>
      <i style="--x:2;--y:0;--z:2.5"></i>
      <i style="--x:3;--y:0;--z:2"></i>
      <i style="--x:0;--y:1;--z:2"></i>
      <i style="--x:1;--y:1;--z:2.5"></i>
      <i style="--x:2;--y:1;--z:2"></i>
      <i style="--x:3;--y:1;--z:1.5"></i>
      <i style="--x:0;--y:2;--z:1"></i>
      <i style="--x:1;--y:2;--z:2"></i>
      <i style="--x:2;--y:2;--z:1.5"></i>
      <i style="--x:3;--y:2;--z:1"></i>
      <i style="--x:0;--y:3;--z:1.5"></i>
      <i style="--x:1;--y:3;--z:1"></i>
      <i style="--x:2;--y:3;--z:1"></i>
      <i style="--x:3;--y:3;--z:0.5" class="pond"></i>
      <i class="rock" style="--x:0.2;--y:0.2;--w:3.6;--h:1.4;--z:0"></i>
      <i class="rock" style="--x:0.7;--y:0.7;--w:2.6;--h:1.3;--z:-1.4"></i>
      <i class="rock" style="--x:1.3;--y:1.3;--w:1.4;--h:1.1;--z:-2.7"></i>
      <i class="trunk" style="--x:1.35;--y:0.35;--w:0.3;--h:0.7;--z:3.7"></i>
      <i class="leaf" style="--x:1.1;--y:0.1;--w:0.8;--h:0.8;--z:4.5"></i>
      <i class="trunk" style="--x:0.35;--y:2.35;--w:0.3;--h:0.5;--z:1.5"></i>
      <i class="leaf" style="--x:0.15;--y:2.15;--w:0.7;--h:0.7;--z:2.2"></i>
    </div>
  </div>
</div>

CSS 103 lines

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

.island {
  --cell: calc(28 * var(--u));
  --unit: calc(14 * var(--u)); /* one step of height */
  position: relative;
  width: calc(200 * var(--u));
  height: calc(190 * var(--u));
  transform-style: preserve-3d;
  /* the land sits in the top of this box and the glow below it: moved down, the solid land is
     centred and the faint glow hangs under it */
  translate: 0 calc(17 * var(--u));
}

.shadow {
  position: absolute;
  left: calc(40 * var(--u));
  bottom: calc(4 * var(--u));
  width: calc(120 * var(--u));
  height: calc(24 * var(--u));
  border-radius: 50%;
  background: radial-gradient(closest-side, rgb(139 108 255 / 0.5), transparent);
  animation: breathe 3.2s ease-in-out infinite alternate;
}

.land {
  position: absolute;
  left: calc(44 * var(--u));
  top: calc(30 * var(--u));
  width: calc(112 * var(--u));
  height: calc(112 * var(--u));
  transform-style: preserve-3d;
  animation: bob 3.2s ease-in-out infinite alternate;
}

/* one block = top face + two walls */
.land i {
  --w: 1;
  --h: var(--z); /* terrain columns reach down to the ground */
  --top: #2bb58f;
  --grass: #1f8a66;
  --side: #8a5f38;
  position: absolute;
  left: calc(var(--x) * var(--cell));
  top: calc(var(--y) * var(--cell));
  width: calc(var(--w) * var(--cell));
  height: calc(var(--w) * var(--cell));
  background: var(--top);
  transform-style: preserve-3d;
  transform: translateZ(calc(var(--z) * var(--unit)));
}

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

/* +y wall, folded down from the bottom edge */
.land i::before {
  top: 100%;
  left: 0;
  width: 100%;
  height: calc(var(--h) * var(--unit));
  background: linear-gradient(var(--grass) 0 calc(3 * var(--u)), var(--side) calc(3 * var(--u)));
  transform-origin: top;
  transform: rotateX(-90deg);
}

/* +x wall, folded down from the right edge, a shade darker */
.land i::after {
  top: 0;
  left: 100%;
  width: calc(var(--h) * var(--unit));
  height: 100%;
  background:
    linear-gradient(rgb(0 0 0 / 0.25), rgb(0 0 0 / 0.25)),
    linear-gradient(90deg, var(--grass) 0 calc(3 * var(--u)), var(--side) calc(3 * var(--u)));
  transform-origin: left;
  transform: rotateY(90deg);
}

.land .pond  { --top: #6f8bff; --grass: #4a5fc4; --side: #3a4a96; }
.land .rock  { --top: #5b5f78; --grass: #6b5a50; --side: #6f7294; }
.land .trunk { --top: #8a5a36; --grass: #6b4228; --side: #6b4228; }
.land .leaf  { --top: #ff9cc6; --grass: #ff4d9d; --side: #ff4d9d; }

@keyframes bob {
  from { transform: translateY(calc(5 * var(--u))) rotateX(58deg) rotateZ(38deg); }
  to   { transform: translateY(calc(-5 * var(--u))) rotateX(58deg) rotateZ(52deg); }
}

@keyframes breathe {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0.55; transform: scale(0.82); }
}
Standalone snippet · plain CSS, no build step, no dependencies.