A pixel-art hourglass: sand pixels leave the top chamber one tick at a time and pile up in the bottom, then the glass flips. Each grain toggles with a stepped keyframe offset by its own delay.
Also known as: 8-bit hourglass loader, pixel art hourglass, retro sand timer loader.
<div class="ldr-pixel-hourglass" role="status" aria-label="Loading"> <span style="--x: 3; --y: 1; --i: 0"></span><span style="--x: 2; --y: 1; --i: 1"></span><span style="--x: 4; --y: 1; --i: 2"></span> <span style="--x: 1; --y: 1; --i: 3"></span><span style="--x: 5; --y: 1; --i: 4"></span><span style="--x: 3; --y: 2; --i: 5"></span> <span style="--x: 2; --y: 2; --i: 6"></span><span style="--x: 4; --y: 2; --i: 7"></span><span style="--x: 3; --y: 3; --i: 8"></span> <span style="--x: 3; --y: 6; --i: 0; --o: 1"></span><span style="--x: 2; --y: 6; --i: 1; --o: 1"></span><span style="--x: 4; --y: 6; --i: 2; --o: 1"></span> <span style="--x: 1; --y: 6; --i: 3; --o: 1"></span><span style="--x: 5; --y: 6; --i: 4; --o: 1"></span><span style="--x: 3; --y: 5; --i: 5; --o: 1"></span> <span style="--x: 2; --y: 5; --i: 6; --o: 1"></span><span style="--x: 4; --y: 5; --i: 7; --o: 1"></span><span style="--x: 3; --y: 4; --i: 8; --o: 1"></span> </div>
.ldr-pixel-hourglass {
--s: var(--ldr-size, 48px);
--c: var(--ldr-color, #3b82f6);
--p: calc(var(--s) / 8);
position: relative;
width: calc(var(--p) * 7);
height: var(--s);
animation: ldr-pixel-hourglass-flip 4s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}
.ldr-pixel-hourglass::before,
.ldr-pixel-hourglass::after {
content: "";
position: absolute;
inset: 0;
}
.ldr-pixel-hourglass::before {
background: color-mix(in srgb, var(--ldr-color, #3b82f6) 14%, transparent);
clip-path: polygon(14.29% 12.5%, 85.71% 12.5%, 85.71% 25%, 71.43% 25%, 71.43% 37.5%, 57.14% 37.5%, 57.14% 62.5%, 71.43% 62.5%, 71.43% 75%, 85.71% 75%, 85.71% 87.5%, 14.29% 87.5%, 14.29% 75%, 28.57% 75%, 28.57% 62.5%, 42.86% 62.5%, 42.86% 37.5%, 28.57% 37.5%, 28.57% 25%, 14.29% 25%);
}
.ldr-pixel-hourglass::after {
background:
linear-gradient(var(--c) 0 0) top / 100% 11% no-repeat,
linear-gradient(var(--c) 0 0) bottom / 100% 11% no-repeat;
}
.ldr-pixel-hourglass span {
--o: 0;
position: absolute;
left: calc(var(--x) * var(--p));
top: calc(var(--y) * var(--p));
width: var(--p);
height: var(--p);
border: calc(var(--p) * 0.08) solid transparent;
background: var(--c) padding-box;
animation: ldr-pixel-hourglass-grain 4s step-end infinite;
animation-delay: calc(var(--i) * 0.16s + var(--o) * 2.08s - 3.88s);
}
@keyframes ldr-pixel-hourglass-grain {
0% { opacity: 0; }
50%, 100% { opacity: 1; }
}
@keyframes ldr-pixel-hourglass-flip {
0%, 40% { transform: rotate(0deg); }
50%, 90% { transform: rotate(180deg); }
100% { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.ldr-pixel-hourglass, .ldr-pixel-hourglass *, .ldr-pixel-hourglass::before, .ldr-pixel-hourglass::after, .ldr-pixel-hourglass *::before, .ldr-pixel-hourglass *::after {
animation-duration: 3s;
}
}--ldr-color on the element or a parent.
Currently previewing #3b82f6.--ldr-size. Every dimension in the snippet is
derived from it. Currently previewing 64px.animation shorthand.
Delays on the children are relative, so they keep their rhythm.role="status" and an
aria-label so screen readers announce the wait. The reduced-motion rule
at the end slows the animation for users who asked for less motion.