Pure CSS Scenes & objects #loop #sass-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.

  • 95 lines of CSS
  • 30 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

How it works

  1. The land is a 4 × 4 grid tilted to an isometric view with rotateX(58deg) rotateZ(45deg). 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.

Key techniques

  • one element per block
  • walls: rotateX(-90deg) / rotateY(90deg) from the top edges
  • Sass @for over a height map
  • 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 95 lines

.scene {
  perspective: 800px;
}

.island {
  --cell: 28px;
  --unit: 14px; /* one step of height */
  position: relative;
  width: 200px;
  height: 190px;
  transform-style: preserve-3d;
}

.shadow {
  position: absolute;
  left: 40px;
  bottom: 4px;
  width: 120px;
  height: 24px;
  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: 44px;
  top: 30px;
  width: 112px;
  height: 112px;
  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 3px, var(--side) 3px);
  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 3px, var(--side) 3px);
  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(5px) rotateX(58deg) rotateZ(38deg); }
  to   { transform: translateY(-5px) rotateX(58deg) rotateZ(52deg); }
}

@keyframes breathe {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0.55; transform: scale(0.82); }
}

Sass source 153 lines

@use 'sass:list';
@use 'sass:math';

// Every block is ONE element: the element is the top face, lifted to --z, and two
// pseudo-elements hang --h units down from its +y and +x edges as walls.
// --x / --y / --w are in grid cells ($cell), --z / --h in height units ($unit).
// The 16 terrain columns get their values from the height map; rocks and trees set theirs inline.
$cell: 28px;
$unit: 14px;
$lip: 3px; // strip of grass at the top of every terrain wall

// row by row, far corner first; 0.5 is the pond
$heights: (
  2, 3, 2.5, 2,
  2, 2.5, 2, 1.5,
  1, 2, 1.5, 1,
  1.5, 1, 1, 0.5
);

.d-island {
  position: relative;
  width: 200px;
  height: 190px;
  transform-style: preserve-3d;

  // a breathing glow under the island: smaller and fainter while the island is up
  &__shadow {
    position: absolute;
    left: 40px;
    bottom: 4px;
    width: 120px;
    height: 24px;
    border-radius: 50%;
    background: radial-gradient(closest-side, color-mix(in srgb, var(--accent) 50%, transparent), transparent);
    animation: d-island-shadow 3.2s ease-in-out infinite alternate;
  }

  &__land {
    position: absolute;
    left: 100px - $cell * 2;
    top: 86px - $cell * 2;
    width: $cell * 4;
    height: $cell * 4;
    transform-style: preserve-3d;
    animation: d-island-bob 3.2s ease-in-out infinite alternate;

    i {
      // grass on top, earth on the walls (terrain columns)
      --top: color-mix(in srgb, var(--accent-2) 70%, #1d6b3a);
      --side: color-mix(in srgb, var(--warm) 45%, #3a2414);
      --grass: color-mix(in srgb, var(--accent-2) 55%, #14512b);
      --w: 1;
      position: absolute;
      left: calc(var(--x) * #{$cell});
      top: calc(var(--y) * #{$cell});
      width: calc(var(--w) * #{$cell});
      height: calc(var(--w) * #{$cell});
      background: var(--top);
      box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.12);
      transform-style: preserve-3d;
      transform: translateZ(calc(var(--z) * #{$unit}));

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

      // +y wall: from the bottom edge, rotateX(-90deg) folds it down
      &::before {
        top: 100%;
        left: 0;
        width: 100%;
        height: calc(var(--h) * #{$unit});
        background: linear-gradient(var(--grass) 0 $lip, var(--side) $lip);
        transform-origin: top;
        transform: rotateX(-90deg);
      }

      // +x wall: from the right edge, rotateY(90deg) folds it down; a shade darker
      &::after {
        top: 0;
        left: 100%;
        width: calc(var(--h) * #{$unit});
        height: 100%;
        background:
          linear-gradient(rgb(0 0 0 / 0.25), rgb(0 0 0 / 0.25)),
          linear-gradient(90deg, var(--grass) 0 $lip, var(--side) $lip);
        transform-origin: left;
        transform: rotateY(90deg);
      }
    }

    @for $n from 1 through 16 {
      $h: list.nth($heights, $n);

      i:nth-child(#{$n}) {
        --x: #{($n - 1) % 4};
        --y: #{math.floor(math.div($n - 1, 4))};
        --z: #{$h};
        --h: #{$h};
      }
    }

    // the pond
    i:nth-child(16) {
      --top: color-mix(in srgb, var(--accent) 55%, #3fa9ff);
      --grass: color-mix(in srgb, var(--accent) 45%, #1d4f99);
      --side: color-mix(in srgb, var(--accent) 40%, #173a6e);
    }

    // stone underside, hanging below the terrain
    .is-rock {
      --top: #5b5f78;
      --grass: color-mix(in srgb, var(--warm) 30%, #4a3a36);
      --side: color-mix(in srgb, var(--muted) 55%, #3a3550);
    }

    .is-trunk {
      --top: #8a5a36;
      --grass: #6b4228;
      --side: #6b4228;
    }

    .is-leaf {
      --top: color-mix(in srgb, var(--hot) 70%, #fff);
      --grass: var(--hot);
      --side: var(--hot);
    }
  }
}

@keyframes d-island-bob {
  from {
    transform: translateY(5px) rotateX(58deg) rotateZ(38deg);
  }

  to {
    transform: translateY(-5px) rotateX(58deg) rotateZ(52deg);
  }
}

@keyframes d-island-shadow {
  from {
    opacity: 1;
    transform: scale(1);
  }

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