Pure CSS Scenes & objects #loop #sass-loop #scene #building

Windmill in pure CSS

A tapered tower of four leaning trapezoids with a pyramid cap, on a grass block. The sails turn with one rotateZ loop while the whole model turns slowly, so the depth reads.

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

How it works

  1. Everything hangs from one point, the middle of the grass block’s top. The outer box tilts the camera down with rotateX(-20deg); a child spins the whole model with rotateY, so you see it from every side.
  2. The tower is a frustum: four trapezoids (clip-path), each standing on its bottom edge. rotateY(side) translateZ(24px) puts it on its side of the base; rotateX(5.59deg) then leans it in by atan((24 − 15) / 92), exactly enough to bring its top edge in to the narrower top.
  3. A leaning side is longer than the tower is high: it is the hypotenuse, √(92² + 9²) = 92.44px. The cap is the same trick with triangles, leaning so far in that the tips meet.
  4. The sails are four identical blades drawn pointing up from the hub (transform-origin = the hub) and turned by 90° steps. Their parent loops rotate(0 → -360deg). It needs preserve-3d too: a flat, zero-size group gets sorted as one plane and hides behind the tower.
  5. Light is baked in: every side has a dark overlay of its own strength (--d), so the model looks lit from the front-left as it turns. backface-visibility: hidden skips the sides facing away.

Key techniques

  • frustum: rotateY → translateZ → rotateX(lean)
  • clip-path trapezoids and triangles
  • rotateZ sails on a hub in front of the cap
  • one model spun with rotateY

Copy-paste code

HTML 30 lines

<div class="scene">
  <div class="windmill">
    <div class="spin">
      <div class="hill">
      <i style="--n:0;--d:0"></i>
      <i style="--n:1;--d:0.2"></i>
      <i style="--n:2;--d:0.36"></i>
      <i style="--n:3;--d:0.12"></i>
        <i class="top"></i>
      </div>
      <div class="tree" style="--x:-50px;--z:-44px;--k:1"><i></i><i></i></div>
      <div class="tree" style="--x:52px;--z:34px;--k:0.8"><i></i><i></i></div>
      <div class="tower">
      <i class="front" style="--n:0;--d:0"></i>
      <i style="--n:1;--d:0.2"></i>
      <i style="--n:2;--d:0.36"></i>
      <i style="--n:3;--d:0.12"></i>
      </div>
      <div class="cap">
      <i style="--n:0;--d:0"></i>
      <i style="--n:1;--d:0.2"></i>
      <i style="--n:2;--d:0.36"></i>
      <i style="--n:3;--d:0.12"></i>
      </div>
      <div class="rotor">
        <div class="sails"><b style="--n:0"></b><b style="--n:1"></b><b style="--n:2"></b><b style="--n:3"></b><u></u></div>
      </div>
    </div>
  </div>
</div>

CSS 169 lines

.scene {
  perspective: 800px;
}

.windmill {
  --grass: color-mix(in srgb, #2ee6d6 35%, #4f9a3a);
  --earth: color-mix(in srgb, #ffb547 30%, #4a3226);
  --plaster: color-mix(in srgb, #ffb547 8%, #f2ede5);
  --roof: color-mix(in srgb, #ff4d9d 70%, #4a1024);
  --wood: #5b3b25;
  position: relative;
  width: 210px;
  height: 200px;
  transform-style: preserve-3d;
  transform: rotateX(-20deg); /* the camera looks down a little */
}

/* the model's origin: the middle of the grass top */
.spin {
  position: absolute;
  left: 50%;
  top: 164px;
  transform-style: preserve-3d;
  animation: turn 28s linear infinite;
}

.hill,
.tower,
.cap,
.rotor,
.tree {
  position: absolute;
  transform-style: preserve-3d;
}

/* grass block: four earth sides hanging from the top edge, and the top laid flat */
.hill i {
  position: absolute;
  left: -75px;
  top: 0;
  width: 150px;
  height: 24px;
  background:
    linear-gradient(rgb(0 0 0 / var(--d)), rgb(0 0 0 / var(--d))),   /* baked-in shade */
    linear-gradient(var(--grass) 0 5px, transparent 7px),
    linear-gradient(var(--earth), color-mix(in srgb, var(--earth) 65%, #000));
  backface-visibility: hidden;
  transform: rotateY(calc(var(--n) * 90deg)) translateZ(75px);
}

.hill .top {
  top: -75px;
  height: 150px;
  background:
    radial-gradient(circle at 60% 30%, #ffe0a8 0 1.5px, transparent 2px) 0 0 / 34px 30px,
    linear-gradient(#c9a066, #c9a066) 50% 100% / 12px 50% no-repeat,   /* the path to the door */
    radial-gradient(closest-side, color-mix(in srgb, var(--grass) 70%, #fff), var(--grass) 70%, color-mix(in srgb, var(--grass) 80%, #000));
  transform: rotateX(90deg);
}

/* a tree: two crossed planes, each cut to a tree outline */
.tree {
  transform: translate3d(var(--x), 0, var(--z)) scale(var(--k));
}

.tree i {
  position: absolute;
  left: -13px;
  top: -44px;
  width: 26px;
  height: 44px;
  background: linear-gradient(color-mix(in srgb, var(--grass) 80%, #fff), color-mix(in srgb, var(--grass) 70%, #000) 80%, var(--wood) 80%);
  clip-path: polygon(50% 0, 70% 32%, 60% 32%, 88% 64%, 74% 64%, 100% 80%, 58% 80%, 58% 100%, 42% 100%, 42% 80%, 0 80%, 26% 64%, 12% 64%, 40% 32%, 30% 32%);
}

.tree i + i {
  transform: rotateY(90deg);
}

/* Tower sides: trapezoids 48px wide at the base, 30px at the top. Each stands on its bottom
   edge (the origin), steps out 24px and leans in by atan(9 / 92) = 5.59deg. */
.tower i {
  position: absolute;
  left: -24px;
  top: -92.44px;
  width: 48px;
  height: 92.44px; /* the slanted length: √(92² + 9²) */
  clip-path: polygon(18.75% 0, 81.25% 0, 100% 100%, 0 100%);
  transform-origin: 50% 100%;
  transform: rotateY(calc(var(--n) * 90deg)) translateZ(24px) rotateX(5.59deg);
  backface-visibility: hidden;
  background:
    linear-gradient(rgb(0 0 0 / var(--d)), rgb(0 0 0 / var(--d))),
    linear-gradient(var(--wood) 0 6px, transparent 6px),
    radial-gradient(circle at 50% 38%, #ffb547 0 3.5px, var(--wood) 4px 5.5px, transparent 6px),
    var(--plaster);
}

.tower .front {
  background:
    linear-gradient(var(--wood) 0 6px, transparent 6px),
    radial-gradient(circle at 50% 38%, #ffb547 0 3.5px, var(--wood) 4px 5.5px, transparent 6px),
    radial-gradient(circle at 50% calc(100% - 16px), var(--wood) 0 7.5px, transparent 8px),
    linear-gradient(var(--wood), var(--wood)) 50% 100% / 15px 16px no-repeat,
    var(--plaster);
}

/* the cap: the same trick with triangles, standing on the tower's top (y = -92px) */
.cap i {
  position: absolute;
  left: -21px;
  top: -128.62px;
  width: 42px;
  height: 36.62px;
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
  transform-origin: 50% 100%;
  transform: rotateY(calc(var(--n) * 90deg)) translateZ(21px) rotateX(34.99deg);
  backface-visibility: hidden;
  background:
    linear-gradient(rgb(0 0 0 / var(--d)), rgb(0 0 0 / var(--d))),
    repeating-linear-gradient(transparent 0 5px, rgb(0 0 0 / 0.22) 5px 6px),
    linear-gradient(color-mix(in srgb, var(--roof) 75%, #fff), var(--roof));
}

/* the hub: above the tower, in front of the cap */
.rotor {
  transform: translate3d(0, -100px, 27px);
}

.sails {
  position: absolute;
  transform-style: preserve-3d; /* sort each sail on its own */
  animation: sails 6s linear infinite;
}

/* one blade pointing up from the hub: stock on the left edge, lattice and cloth beside it */
.sails b {
  position: absolute;
  left: -2px;
  bottom: 5px;
  width: 24px;
  height: 70px;
  transform-origin: 2px calc(100% + 5px); /* the hub */
  transform: rotate(calc(var(--n) * 90deg));
  background:
    linear-gradient(90deg, var(--wood) 0 4px, transparent 4px),
    linear-gradient(90deg, transparent calc(100% - 2px), var(--wood) 0) 0 0 / 100% 80% no-repeat,
    repeating-linear-gradient(transparent 0 7px, var(--wood) 7px 8.5px) 0 0 / 100% 80% no-repeat,
    linear-gradient(#fff6e9, #efd5ab) 4px 0 / calc(100% - 4px) 80% no-repeat;
}

.sails u {
  position: absolute;
  left: -6px;
  top: -6px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, #c8b5a8, var(--wood) 70%);
  transform: translateZ(1px);
}

@keyframes turn {
  to { transform: rotateY(360deg); }
}

@keyframes sails {
  to { transform: rotate(-360deg); }
}

Sass source 246 lines

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

// The whole model hangs from one point: the middle of the grass block's top face (__spin).
// The root holds the camera tilt; __spin turns the model; every part is placed from that point.
//
// A tapered side (tower or cap) stands on its bottom edge: rotateY(side) → translateZ(half-width
// at the base) → rotateX(lean), with transform-origin at that bottom edge. Leaning by
// atan((base - top) / height) brings its top edge in to the top half-width, and its slanted
// length is the hypotenuse, so the height comes out exact.
$hill: 150px;
$hill-h: 24px;

$base: 24; // tower half-width at the ground
$top: 15; // … at the top
$tower-h: 92;
$tower-lean: math.atan(math.div($base - $top, $tower-h));
$tower-side: math.sqrt($tower-h * $tower-h + ($base - $top) * ($base - $top)) * 1px;
$tower-in: math.div($base - $top, 2 * $base) * 100%; // clip inset at the top corners

$cap: 21; // pyramid cap: half-width, height
$cap-h: 30;
$cap-lean: math.atan(math.div($cap, $cap-h));
$cap-side: math.sqrt($cap * $cap + $cap-h * $cap-h) * 1px;

$hub-y: ($tower-h + 8) * -1px; // the sails' hub: a little above the tower, in front of the cap
$hub-z: 27px;
$wood: #5b3b25;
$shade: (0, 0.2, 0.36, 0.12); // front, right, back, left

.d-windmill {
  --grass: color-mix(in srgb, var(--accent-2) 35%, #4f9a3a);
  --earth: color-mix(in srgb, var(--warm) 30%, #4a3226);
  --plaster: color-mix(in srgb, var(--warm) 8%, #f2ede5);
  --roof: color-mix(in srgb, var(--hot) 70%, #4a1024);
  position: relative;
  width: 210px;
  height: 200px;
  transform-style: preserve-3d;
  transform: rotateX(-20deg);

  &__spin {
    position: absolute;
    left: 50%;
    top: 164px;
    transform-style: preserve-3d;
    animation: d-windmill-turn 28s linear infinite;
  }

  // the grass block: four earth sides and a top
  &__hill {
    position: absolute;
    transform-style: preserve-3d;

    i {
      position: absolute;
      left: $hill * -0.5;
      top: 0;
      width: $hill;
      height: $hill-h;
      background:
        linear-gradient(rgb(0 0 0 / var(--d, 0)), rgb(0 0 0 / var(--d, 0))),
        linear-gradient(var(--grass) 0 5px, color-mix(in srgb, var(--grass) 50%, var(--earth)) 5px 7px, transparent 7px),
        radial-gradient(circle at 20% 60%, rgb(0 0 0 / 0.18) 0 2px, transparent 2.5px) 0 0 / 23px 100%,
        radial-gradient(circle at 70% 80%, rgb(255 255 255 / 0.1) 0 1.5px, transparent 2px) 0 0 / 31px 100%,
        linear-gradient(var(--earth), color-mix(in srgb, var(--earth) 65%, #000));
      backface-visibility: hidden;
    }

    @for $n from 1 through 4 {
      i:nth-child(#{$n}) {
        --d: #{list.nth($shade, $n)};
        transform: rotateY(($n - 1) * 90deg) translateZ($hill * 0.5);
      }
    }

    // the top, laid flat: a lighter mound in the middle and a path to the door (front = bottom edge)
    i:nth-child(5) {
      top: $hill * -0.5;
      height: $hill;
      background:
        radial-gradient(circle at 30% 70%, #fff6 0 1.5px, transparent 2px) 0 0 / 26px 22px,
        radial-gradient(circle at 60% 30%, color-mix(in srgb, var(--warm) 80%, #fff) 0 1.5px, transparent 2px) 0 0 / 34px 30px,
        linear-gradient(color-mix(in srgb, var(--warm) 25%, #b89a6e), color-mix(in srgb, var(--warm) 25%, #b89a6e)) 50% 100% / 12px 50% no-repeat,
        radial-gradient(closest-side, color-mix(in srgb, var(--grass) 70%, #fff), var(--grass) 70%, color-mix(in srgb, var(--grass) 80%, #000));
      transform: rotateX(90deg);
    }
  }

  // conifers: two crossed planes, each a tree outline cut with clip-path
  &__tree {
    position: absolute;
    transform-style: preserve-3d;
    transform: translate3d(var(--x), 0, var(--z)) scale(var(--k));

    i {
      position: absolute;
      left: -13px;
      top: -44px;
      width: 26px;
      height: 44px;
      background: linear-gradient(color-mix(in srgb, var(--grass) 80%, #fff) , color-mix(in srgb, var(--grass) 70%, #000) 80%, $wood 80%);
      clip-path: polygon(50% 0, 70% 32%, 60% 32%, 88% 64%, 74% 64%, 100% 80%, 58% 80%, 58% 100%, 42% 100%, 42% 80%, 0 80%, 26% 64%, 12% 64%, 40% 32%, 30% 32%);
    }

    i + i {
      transform: rotateY(90deg);
    }
  }

  &__tower {
    position: absolute;
    transform-style: preserve-3d;

    i {
      position: absolute;
      left: $base * -1px;
      top: -$tower-side;
      width: $base * 2px;
      height: $tower-side;
      clip-path: polygon($tower-in 0, 100% - $tower-in 0, 100% 100%, 0 100%);
      transform-origin: 50% 100%;
      backface-visibility: hidden; // closed solids: far sides are never drawn, so never mis-sorted
      background:
        linear-gradient(rgb(0 0 0 / var(--d, 0)), rgb(0 0 0 / var(--d, 0))),
        linear-gradient($wood 0 6px, transparent 6px),
        radial-gradient(circle at 50% 38%, var(--warm) 0 3.5px, $wood 4px 5.5px, transparent 6px),
        repeating-linear-gradient(transparent 0 11px, rgb(0 0 0 / 0.06) 11px 12px),
        var(--plaster);
    }

    @for $n from 1 through 4 {
      i:nth-child(#{$n}) {
        --d: #{list.nth($shade, $n)};
        transform: rotateY(($n - 1) * 90deg) translateZ($base * 1px) rotateX($tower-lean);
      }
    }

    // the front: an arched door under the window
    i:first-child {
      background:
        linear-gradient($wood 0 6px, transparent 6px),
        radial-gradient(circle at 50% 38%, var(--warm) 0 3.5px, $wood 4px 5.5px, transparent 6px),
        radial-gradient(circle at 50% calc(100% - 16px), $wood 0 7.5px, transparent 8px),
        linear-gradient($wood, $wood) 50% 100% / 15px 16px no-repeat,
        repeating-linear-gradient(transparent 0 11px, rgb(0 0 0 / 0.06) 11px 12px),
        var(--plaster);
    }
  }

  &__cap {
    position: absolute;
    transform-style: preserve-3d;

    i {
      position: absolute;
      left: $cap * -1px;
      top: $tower-h * -1px - $cap-side;
      width: $cap * 2px;
      height: $cap-side;
      clip-path: polygon(50% 0, 100% 100%, 0 100%);
      transform-origin: 50% 100%;
      backface-visibility: hidden;
      background:
        linear-gradient(rgb(0 0 0 / var(--d, 0)), rgb(0 0 0 / var(--d, 0))),
        repeating-linear-gradient(transparent 0 5px, rgb(0 0 0 / 0.22) 5px 6px),
        linear-gradient(color-mix(in srgb, var(--roof) 75%, #fff), var(--roof));
    }

    @for $n from 1 through 4 {
      i:nth-child(#{$n}) {
        --d: #{list.nth($shade, $n)};
        transform: rotateY(($n - 1) * 90deg) translateZ($cap * 1px) rotateX($cap-lean);
      }
    }
  }

  // the hub point, with an axle reaching back into the cap
  &__rotor {
    position: absolute;
    transform-style: preserve-3d;
    transform: translate3d(0, $hub-y, $hub-z);

    &::before {
      content: '';
      position: absolute;
      left: -2.5px;
      top: -9px;
      width: 5px;
      height: 18px;
      background: $wood;
      transform: translateZ(-9px) rotateX(90deg);
    }
  }

  // Four sails around the hub. Each one is drawn pointing up from the hub (stock on its left
  // edge, lattice and cloth beside it) and turned by 90deg steps around the hub point.
  &__sails {
    position: absolute;
    transform-style: preserve-3d; // each sail is sorted on its own (a 0 × 0 flat group is not)
    animation: d-windmill-sails 6s linear infinite;

    b {
      position: absolute;
      left: -2px;
      bottom: 5px;
      width: 24px;
      height: 70px;
      transform-origin: 2px calc(100% + 5px); // = the hub
      background:
        linear-gradient(90deg, $wood 0 4px, transparent 4px),
        linear-gradient(90deg, transparent calc(100% - 2px), $wood 0) 0 0 / 100% 80% no-repeat,
        repeating-linear-gradient(transparent 0 7px, $wood 7px 8.5px) 0 0 / 100% 80% no-repeat,
        linear-gradient(color-mix(in srgb, var(--warm) 12%, #fff), color-mix(in srgb, var(--warm) 30%, #e8e2d6)) 4px 0 / calc(100% - 4px) 80% no-repeat;
    }

    @for $n from 2 through 4 {
      b:nth-child(#{$n}) {
        transform: rotate(($n - 1) * 90deg);
      }
    }

    u {
      position: absolute;
      left: -6px;
      top: -6px;
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: radial-gradient(circle at 35% 35%, color-mix(in srgb, $wood 40%, #fff), $wood 70%);
      transform: translateZ(1px);
    }
  }
}

@keyframes d-windmill-turn {
  to {
    transform: rotateY(360deg);
  }
}

@keyframes d-windmill-sails {
  to {
    transform: rotate(-360deg);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.