Hourglass Flip Loader
A CSS hourglass loader comparing two implementation methods: a real drain-pause-flip cycle and a CSS-only fade reset, both inside a fixed badge footprint.
A CSS hourglass loader comparing two implementation methods: a real drain-pause-flip cycle and a CSS-only fade reset, both inside a fixed badge footprint.
Loader / flip vs fade reset
Two renderings of the same hourglass badge compare the implementation method directly: a real drain-pause-flip cycle and a CSS-only fade reset that hides the refill jump.
Drain -> pause -> rotate
A stateful drain cycle flips the actual hourglass after the sand transfer completes.
CSS drain -> opacity reset
A CSS-only loop hides the reset with opacity instead of physically rotating the hourglass.
A CSS hourglass loader rotates a fixed-size SVG vessel around its center while clipped sand layers translate inside the bulbs, so the waiting state loops without layout movement.
1<div class="hourglass-loader" role="status" aria-live="polite"><span class="hourglass-loader__icon" aria-hidden="true"><span class="hourglass-loader__rotor"><svg class="hourglass-loader__svg" viewBox="0 0 64 80" focusable="false"><defs><clipPath id="hourglass-top"><path d="M14 8 L50 8 L50 12 C50 24 38 32 32 38 C26 32 14 24 14 12 Z" /></clipPath><clipPath id="hourglass-bottom"><path d="M32 42 C38 48 50 56 50 68 L50 72 L14 72 L14 68 C14 56 26 48 32 42 Z" /></clipPath></defs><rect class="hourglass-loader__cap" x="10" y="5" width="44" height="5" rx="2.5" /><rect class="hourglass-loader__cap" x="10" y="70" width="44" height="5" rx="2.5" /><g clip-path="url(#hourglass-top)"><rect class="hourglass-loader__sand hourglass-loader__sand--top" x="10" y="6" width="44" height="36" /></g><g clip-path="url(#hourglass-bottom)"><rect class="hourglass-loader__sand hourglass-loader__sand--bottom" x="10" y="40" width="44" height="36" /></g><line class="hourglass-loader__stream" x1="32" y1="38" x2="32" y2="44" /><path class="hourglass-loader__glass" d="M14 8 L50 8 L50 12 C50 24 38 32 32 40 C26 32 14 24 14 12 Z" /><path class="hourglass-loader__glass" d="M32 40 C38 48 50 56 50 68 L50 72 L14 72 L14 68 C14 56 26 48 32 40 Z" /></svg></span></span><span class="hourglass-loader__label">Pending</span></div><style>.hourglass-loader {display: inline-flex;align-items: center;gap: .45rem;min-height: 5.75rem;padding: .75rem 1rem .75rem .75rem;border: 1px solid rgba(103, 232, 249, .28);border-radius: 1rem;background: rgba(10, 16, 30, .78);color: #e0f2fe;}42.hourglass-loader__icon {width: 92px;height: 92px;display: grid;place-items: center;contain: layout paint;}.hourglass-loader__rotor {width: 64px;height: 64px;display: grid;place-items: center;54transform-origin: center;animation: hourglass-real-flip 4.8s cubic-bezier(.45,.05,.3,1) infinite;}.hourglass-loader__svg {display: block;width: 64px;height: 64px;overflow: visible;}.hourglass-loader__glass {fill: none;stroke: #67e8f9;stroke-width: 3.2;stroke-linejoin: round;stroke-linecap: round;}.hourglass-loader__cap {fill: #a78bfa;}73.hourglass-loader__sand {fill: #99f6e4;transform-box: fill-box;}.hourglass-loader__sand--top {78animation: hourglass-top-sand 4.8s linear infinite;}.hourglass-loader__sand--bottom {transform: translateY(36px);animation: hourglass-bottom-sand 4.8s linear infinite;}.hourglass-loader__stream {opacity: 0;stroke: #99f6e4;stroke-width: 2.4;stroke-linecap: round;animation: hourglass-stream 4.8s ease-in-out infinite;}91@keyframes hourglass-real-flip {0%, 85.4167% { transform: rotate(0deg); }100% { transform: rotate(180deg); }}@keyframes hourglass-top-sand {0% { transform: translateY(0); }75%, 100% { transform: translateY(36px); }}@keyframes hourglass-bottom-sand {0% { transform: translateY(36px); }75%, 100% { transform: translateY(0); }}@keyframes hourglass-stream {0%, 93%, 100% { opacity: 0; }3%, 92% { opacity: 1; }}107@media (prefers-reduced-motion: reduce) {.hourglass-loader__rotor,.hourglass-loader__sand,.hourglass-loader__stream {animation: none;}}</style>
No. Announce the loading state once with role="status" and aria-live="polite"; hide the decorative hourglass with aria-hidden so assistive tech is not spammed by a looping visual.
The rotating silhouette needs room for its largest visual extent. A stable wrapper prevents clipping in cards and prevents neighboring inline content from shifting while the vessel turns.
Both cells rotate the same hourglass. BEFORE pivots from the top edge, so the loader swings out of its reserved box. AFTER uses transform-origin: center, so the 180-degree flip stays centered.
The top and bottom bulbs are supposed to trade places in one reserved box. A top or corner origin changes the animation into a swing and often clips outside the loader footprint.
BEFORE ties the loader footprint to animated height, so the badge itself grows and shrinks. AFTER keeps the same SVG footprint fixed and moves clipped sand with translateY.
Height changes can resize rows or push UI below the loader. Put the sand in fixed, clipped SVG bulbs and animate transform: translateY() so the fill changes visually without reflow.
Use one transfer keyframe and reverse the bottom layer, or drive both layers from the same custom property. Independent timings drift and make the fill handoff look broken.
Use it when the brand or product wants an object-state waiting metaphor. For dense system UI, circular spinners or native progress usually scan faster and consume less visual attention.
Show a static hourglass and the pending text. Do not replace the flip with a fade or a slower spin; motion-sensitive users asked for less motion, not a different loop.
An hourglass loader is an object-state loop rather than a simple spinner. This page compares two ways to close that loop: a real drain-pause-flip cycle that rotates the actual vessel, and a CSS-only fade reset that hides the refill jump. Both methods still need to behave like a compact UI loader: one fixed footprint, one status label, and no layout movement.
Use this for short, indefinite waits where a little character is welcome: generating a report, preparing a preview, or waiting for a batch step to complete. Skip it in dense tables, navigation chrome, or repeated list rows where a circular spinner or native progress element scans faster. If the system knows real progress, use a determinate progress indicator instead of an hourglass loop.
The vessel sits in a fixed-size wrapper with transform-origin: center. The frame is an SVG hourglass, each bulb is clipped with clipPath, and the sand rectangles move with translateY(). In the real flip method, JavaScript waits for the drain phase, pauses, then rotates the vessel by 180 degrees. In the fade reset method, CSS keyframes keep the vessel upright and briefly fade the graphic while the sand jumps back to its starting state.
The common failures are wrong pivot, visible reset jumps, and layout-sized sand. A top-origin rotation swings the vessel outside its box, so the loader clips or bumps into neighbors. Animating sand height can resize the row below it. Keep the wrapper dimensions stable, move clipped sand layers with transforms, and make the method explicit: either truly flip after the drain phase, or hide the reset with opacity.
The hourglass is decorative. Put the text state in a wrapper with role="status" and aria-live="polite", then mark the hourglass graphic aria-hidden. Reduced motion should freeze the vessel and sand in a clear pending state rather than swapping to another animation. Stop the loop when the real async state resolves, fails, or times out.
An hourglass loader differs from circular spinners because it is an object-state loop: a fixed two-bulb vessel rotates around its center while clipped sand layers imply transfer between states. The important mechanic is not rotation alone, but center-origin rotation plus sand fill handoff inside a stable box.
Production issues usually come from the wrong pivot or from animating layout-sized sand. Keep transform-origin at center, reserve the loader footprint, animate clipped transform layers rather than height, and expose a single polite status message so the loop stays visual instead of becoming repeated assistive-tech noise.