← Back to gallery
CSS

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.

hourglassloadertransform-originrotatefade-resettranslateYclip-patharia-liveprefers-reduced-motion

Loader / flip vs fade reset

Hourglass Flip Loader

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

Real Flip

A stateful drain cycle flips the actual hourglass after the sand transfer completes.

  • real flip
  • stateful cycle
  • translateY sand

CSS drain -> opacity reset

Fade Reset

A CSS-only loop hides the reset with opacity instead of physically rotating the hourglass.

  • fade reset
  • css keyframes
  • hidden refill

Hourglass method inspector

Real Flip

Pending
  • real flip
  • stateful cycle
  • translateY sand

Sand drains with translateY, the state pauses, then JavaScript rotates the vessel by 180 degrees so the same DOM truly flips.

Helped you ship something? 🐟 Send my cat a churu

.hourglass-loader {
  --drain: 3.6s;
  --pause: 0.50s;
  --flip: .7s;
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: 14px 26px 14px 14px;
  border: 1px solid rgba(103, 232, 249, .28);
  border-radius: 18px;
  background: rgba(10, 16, 30, .78);
  color: #e0f2fe;
}

.hourglass-loader__icon {
  width: 92px;
  height: 92px;
  display: grid;
  place-items: center;
}

.hourglass-loader__rotor {
  width: 64px;
  height: 64px;
  display: grid;
  place-items: center;
  transform-origin: 50% 50%;
  transition: transform var(--flip) cubic-bezier(.45, .05, .3, 1);
}

.hourglass-loader.is-flipped .hourglass-loader__rotor {
  transform: rotate(var(--turns, 180deg));
}

.glass {
  fill: none;
  stroke: #67e8f9;
  stroke-width: 3.2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.cap {
  fill: #a78bfa;
}

.sand {
  fill: #99f6e4;
}

.stream {
  opacity: 0;
  stroke: #99f6e4;
  stroke-width: 2.4;
  stroke-linecap: round;
}

.top-sand,
.bottom-sand {
  transform-box: fill-box;
}

.bottom-sand {
  transform: translateY(36px);
}

.hourglass-loader.is-reversed .top-sand {
  transform: translateY(-36px);
}

.hourglass-loader.is-reversed .bottom-sand {
  transform: translateY(0);
}

.hourglass-loader.is-draining .stream {
  opacity: 1;
}

.hourglass-loader.is-draining:not(.is-reversed) .top-sand {
  transform: translateY(36px);
  transition: transform var(--drain) linear;
}

.hourglass-loader.is-draining:not(.is-reversed) .bottom-sand {
  transform: translateY(0);
  transition: transform var(--drain) linear;
}

.hourglass-loader.is-draining.is-reversed .top-sand {
  transform: translateY(0);
  transition: transform var(--drain) linear;
}

.hourglass-loader.is-draining.is-reversed .bottom-sand {
  transform: translateY(-36px);
  transition: transform var(--drain) linear;
}

@media (prefers-reduced-motion: reduce) {
  .hourglass-loader__rotor,
  .top-sand,
  .bottom-sand {
    transition: none;
    transform: none;
  }
}

How to make this

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.

html
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;54  transform-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 {78  animation: 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>

Annotated snippet

  1. Line 1Expose one polite status for the whole loader. The hourglass shape is decorative, so it stays aria-hidden and does not announce every flip.
    PitfallShould an hourglass loader announce every flip?

    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.

  2. Line 42The icon slot reserves the loader footprint. Without a stable wrapper, a rotating hourglass can clip or make adjacent inline content jump.
    PitfallWhy does the hourglass need a fixed wrapper?

    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.

  3. Line 54Center pivot is the core mechanic. A top or corner pivot makes the vessel swing like a hanging sign instead of trading the top and bottom bulbs in place.

    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.

    PitfallWhy must the hourglass flip around the center?

    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.

  4. Line 73The sand lives inside fixed SVG bulbs and moves with translateY. That keeps the loader usable in a toolbar, card, or table row without resizing the parent.

    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.

    PitfallWhy animate sand with transform instead of height?

    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.

  5. Line 78Both top and bottom fills share the same 4.8s phase model. The top drains while the bottom fills, so the handoff stays synchronized.
    PitfallHow do I keep top and bottom sand in sync?

    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.

  6. Line 91The flip keyframe holds the start and end states so users can read the hourglass instead of seeing continuous spin. That pause is what separates this from a circular spinner.
    PitfallWhen should I use an hourglass instead of a spinner?

    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.

  7. Line 107Reduced motion freezes both the vessel and sand. Keep the status label visible so the pending state survives without movement.
    PitfallWhat should reduced motion show?

    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.

Other pitfalls

When should an indefinite hourglass loader stop?
Stop when the async state resolves or fails, and provide a timeout or retry path for long waits. An indefinite loader that survives an error is misleading status.

Notes

Overview

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.

When to use it

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.

How it works

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.

Production gotchas

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.

Accessibility

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.

References

Implementation depth

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.