Pure CSS Loaders & patterns #loop #loader #spinner

Folding-square loader in pure CSS

The classic fold loader laid on a floor in real perspective: each quadrant swings up over one edge, rests, then folds away over the next, a quarter-beat after its neighbour.

  • 81 lines of CSS
  • 8 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. All four flaps are the same top-left quadrant, just turned around the loader's centre in 90° steps with rotateZ(calc(var(--i) * 90deg)) — one keyframe animation drives all four.
  2. The flap is hinged on the corner that touches the centre: transform-origin: calc(100% + 2 * var(--u)) calc(100% + 2 * var(--u)), just outside its own corner — --u is the one base unit every length here is a multiple of.
  3. A negative animation-delay (calc(var(--i) * 0.3s - 1.65s)) starts each flap already part-way through the cycle instead of waiting its turn, which is what makes the four look like they are chasing each other. The 1.65s is picked so that at the very first frame all four are lying in their rest window (25–75%), so a paused card shows the whole square, not two flaps mid-chase.
  4. Both ends of the keyframe are opacity: 0, so the fold-in and fold-out happen off-screen — the loop has no visible seam.
  5. Laying the whole thing flat with rotateX(44deg) rotateZ(45deg) turns a normally flat spinner into flaps that visibly stand up off a floor. The rotateZ already makes the plate √2 as wide as it is deep, so the tilt stays well off a true isometric 58° — otherwise the spinner would read as a wide, flat smear.

Key techniques

  • one quadrant rotated 4× with --i
  • transform-origin on the shared corner
  • negative animation-delay chase
  • invisible at both ends = seamless

Copy-paste code

HTML 8 lines

<div class="scene">
  <div class="cubeloader" role="img" aria-label="Loading">
    <i style="--i:0;--c:#8b6cff"></i>
    <i style="--i:1;--c:#ff4d9d"></i>
    <i style="--i:2;--c:#2ee6d6"></i>
    <i style="--i:3;--c:#ffb547"></i>
  </div>
</div>

CSS 81 lines

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

/* the classic "folding cube" spinner, but laid on a floor in real perspective so the flaps
   genuinely stand up out of the plane while they fold. The tilt is 44°, not the 58° of a true
   isometric floor: turned 45° in its own plane the plate is already √2 as wide as it is deep, and
   a flatter camera would leave the whole thing too short to read. */
.cubeloader {
  --s: calc(92 * var(--u));
  position: relative;
  width: var(--s);
  height: var(--s);
  transform-style: preserve-3d;
  transform: rotateX(44deg) rotateZ(45deg);
}

/* the floor plate */
.cubeloader::before {
  content: '';
  position: absolute;
  inset: calc(-9 * var(--u));
  border: calc(1 * var(--u)) dashed rgb(140 150 220 / 0.34);
  border-radius: calc(12 * var(--u));
  background: color-mix(in srgb, #8b6cff 9%, transparent);
  transform: translateZ(calc(-1 * var(--u)));
}

/* all four quadrants are the SAME top-left square, turned around the loader's centre in
   90° steps (--i = 0…3), so one keyframe animation serves all four */
.cubeloader i {
  position: absolute;
  top: 0;
  left: 0;
  width: 50%;
  height: 50%;
  transform-origin: 100% 100%;
  transform: rotateZ(calc(var(--i) * 90deg));
  transform-style: preserve-3d;
}

/* the flap, hinged on the corner that touches the loader's centre */
.cubeloader i::before {
  content: '';
  position: absolute;
  inset: calc(2 * var(--u));
  border-radius: calc(5 * var(--u));
  transform-origin: calc(100% + 2 * var(--u)) calc(100% + 2 * var(--u));
  /* a negative delay starts each flap part-way through: no waiting, and the four run in a chase.
     At the first frame flap 0 is 69% through and flap 3 31%: all four resting, the square whole */
  animation: cubeloader-fold 2.4s ease-in-out calc(var(--i) * 0.3s - 1.65s) infinite;
  background: color-mix(in srgb, var(--c) 45%, transparent);
  border: calc(1 * var(--u)) solid color-mix(in srgb, var(--c) 75%, transparent);
  box-shadow: inset 0 0 calc(24 * var(--u)) color-mix(in srgb, var(--c) 30%, transparent);
}

/* fold in over the bottom edge, rest, fold out over the right edge. Both ends are invisible,
   so the loop has no seam. Both functions appear in every keyframe so the browser interpolates
   the two angles separately instead of blending matrices */
@keyframes cubeloader-fold {
  0%,
  10% {
    opacity: 0;
    transform: rotateX(-180deg) rotateY(0deg);
  }

  25%,
  75% {
    opacity: 1;
    transform: rotateX(0deg) rotateY(0deg);
  }

  90%,
  100% {
    opacity: 0;
    transform: rotateX(0deg) rotateY(180deg);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.