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.

  • 75 lines of CSS
  • 8 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed
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% + 2px) calc(100% + 2px), just outside its own corner.
  3. A negative animation-delay (calc(var(--i) * 0.3s - 2.4s)) 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.
  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(58deg) rotateZ(45deg) turns a normally flat spinner into flaps that visibly stand up off a floor.

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 75 lines

.scene {
  perspective: 800px;
}

/* 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 */
.cubeloader {
  --s: 92px;
  position: relative;
  width: var(--s);
  height: var(--s);
  transform-style: preserve-3d;
  transform: rotateX(58deg) rotateZ(45deg);
}

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

/* 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: 2px;
  border-radius: 5px;
  transform-origin: calc(100% + 2px) calc(100% + 2px);
  /* a negative delay starts each flap part-way through: no waiting, and the four run in a chase */
  animation: cubeloader-fold 2.4s ease-in-out calc(var(--i) * 0.3s - 2.4s) infinite;
  background: color-mix(in srgb, var(--c) 45%, transparent);
  border: 1px solid color-mix(in srgb, var(--c) 75%, transparent);
  box-shadow: inset 0 0 24px 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);
  }
}

Sass source 73 lines

@use '../mixins' as *;

$time: 2.4s;

// 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.
.d-cubeloader {
  --s: 92px;
  position: relative;
  width: var(--s);
  height: var(--s);
  transform-style: preserve-3d;
  transform: rotateX(58deg) rotateZ(45deg);

  // the floor plate
  &::before {
    content: '';
    position: absolute;
    inset: -9px;
    border: 1px dashed var(--border-strong);
    border-radius: 12px;
    background: color-mix(in srgb, var(--accent) 9%, transparent);
    transform: translateZ(-1px);
  }

  // All four quadrants are the SAME top-left square, turned around the centre of the loader in
  // 90° steps (--i = 0…3). So one keyframe animation serves all four.
  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
    &::before {
      content: '';
      position: absolute;
      inset: 2px;
      border-radius: 5px;
      transform-origin: calc(100% + 2px) calc(100% + 2px);
      // a negative delay starts each flap part-way through: no waiting, and the four run in a chase
      animation: d-cubeloader-fold $time ease-in-out calc(var(--i) * #{$time * 0.125} - #{$time}) infinite;
      @include face(var(--c), 45%);
    }
  }
}

// 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 d-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.