Pure CSS Loaders & patterns #loop #loader

Flipping loader in pure CSS

The classic square loader: half a turn on X, then half a turn on Y, and both again, so the square ends the loop exactly where it began.

  • 21 lines of CSS
  • 1 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. One element, no wrapper: the perspective() function goes inside the transform itself.
  2. It must come first in the transform list, and must be repeated in every keyframe.
  3. Step 1 flips on X, step 2 flips on Y while X stays at −180°. Two half turns leave the square upside down (the same as a half turn on Z), so its gradient would jump at the seam; steps 3 and 4 flip X and Y once more, and after four the square is back exactly where it started.
  4. Use this form when you cannot add a parent just to hold perspective.
  5. Every length is a multiple of one base unit, --u, tied to the canvas — the perspective() distance too, 260 units — so the square and the depth of its flip are the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • perspective() inside transform
  • multi-step @keyframes
  • no wrapper needed

Copy-paste code

HTML 1 lines

<div class="loader"></div>

CSS 21 lines

.loader {
  /* one base unit: every length below is a multiple of it, so the loader is the same share of a
     gallery card, the editor, a full screen and a recording canvas */
  --u: 0.46vmin;
  width: calc(90 * var(--u));
  height: calc(90 * var(--u));
  border-radius: calc(12 * var(--u));
  background: linear-gradient(135deg, #2ee6d6, #8b6cff);
  box-shadow: 0 0 calc(30 * var(--u)) rgb(139 108 255 / 0.6);
  /* each step is a 0.9s half turn, eased on its own */
  animation: flip 3.6s ease-in-out infinite;
}

/* the perspective() function scales with the loader too, so the flip keeps its depth */
@keyframes flip {
  0%   { transform: perspective(calc(260 * var(--u))) rotateX(0deg)    rotateY(0deg); }
  25%  { transform: perspective(calc(260 * var(--u))) rotateX(-180deg) rotateY(0deg); }
  50%  { transform: perspective(calc(260 * var(--u))) rotateX(-180deg) rotateY(-180deg); }
  75%  { transform: perspective(calc(260 * var(--u))) rotateX(-360deg) rotateY(-180deg); }
  100% { transform: perspective(calc(260 * var(--u))) rotateX(-360deg) rotateY(-360deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.