AI CSS Animations

Pulley Loader

Two weights hang over a pulley and trade places, one sinking as the other rises, while the wheel turns by exactly the length of rope that passes over it. Ropes scale from the top.

Also known as: pulley animation, rope and pulley loader, atwood machine animation.

HTML

<div class="ldr-pulley" role="status" aria-label="Loading">
  <span class="ldr-pulley-rope"></span>
  <span class="ldr-pulley-rope"></span>
  <span class="ldr-pulley-weight"></span>
  <span class="ldr-pulley-weight"></span>
  <span class="ldr-pulley-wheel"></span>
</div>

CSS

.ldr-pulley {
  --s: var(--ldr-size, 48px);
  --c: var(--ldr-color, #3b82f6);
  position: relative;
  width: calc(var(--s) * 0.9);
  height: calc(var(--s) * 1.2);
}
.ldr-pulley::before {
  content: "";
  position: absolute;
  top: 0;
  left: 25%;
  right: 25%;
  height: calc(var(--s) / 16);
  border-radius: calc(var(--s) / 32);
  background: color-mix(in srgb, var(--ldr-color, #3b82f6) 45%, transparent);
}
.ldr-pulley::after {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  width: calc(var(--s) / 20);
  height: calc(var(--s) * 0.3);
  margin-left: calc(var(--s) / -40);
  background: color-mix(in srgb, var(--ldr-color, #3b82f6) 45%, transparent);
}
.ldr-pulley-wheel {
  position: absolute;
  top: calc(var(--s) * 0.05);
  left: 50%;
  width: calc(var(--s) * 0.5);
  height: calc(var(--s) * 0.5);
  margin-left: calc(var(--s) * -0.25);
  box-sizing: border-box;
  border: calc(var(--s) / 16) solid var(--c);
  border-radius: 50%;
  background:
    radial-gradient(circle, var(--c) 0 calc(var(--s) / 16), transparent 0),
    linear-gradient(0deg, transparent calc(50% - 1px), color-mix(in srgb, var(--ldr-color, #3b82f6) 50%, transparent) 0 calc(50% + 1px), transparent 0),
    linear-gradient(90deg, transparent calc(50% - 1px), color-mix(in srgb, var(--ldr-color, #3b82f6) 50%, transparent) 0 calc(50% + 1px), transparent 0);
  animation: ldr-pulley-turn 1.2s cubic-bezier(0.37, 0, 0.63, 1) infinite alternate;
}
.ldr-pulley-rope {
  position: absolute;
  top: calc(var(--s) * 0.3);
  left: calc(50% - var(--s) * 0.25);
  width: 1px;
  height: calc(var(--s) * 0.6);
  background: color-mix(in srgb, var(--ldr-color, #3b82f6) 70%, transparent);
  transform-origin: 50% 0;
  animation: ldr-pulley-rope 1.2s cubic-bezier(0.37, 0, 0.63, 1) infinite alternate;
}
.ldr-pulley-weight {
  position: absolute;
  top: calc(var(--s) * 0.3);
  left: calc(50% - var(--s) * 0.38);
  width: calc(var(--s) * 0.26);
  height: calc(var(--s) * 0.26);
  border-radius: calc(var(--s) / 24);
  background: var(--c);
  animation: ldr-pulley-lift 1.2s cubic-bezier(0.37, 0, 0.63, 1) infinite alternate;
}
/* The right-hand rope and weight run the same keyframes backwards. */
.ldr-pulley-rope + .ldr-pulley-rope {
  left: calc(50% + var(--s) * 0.25 - 1px);
  animation-direction: alternate-reverse;
}
.ldr-pulley-weight + .ldr-pulley-weight {
  left: calc(50% + var(--s) * 0.15);
  width: calc(var(--s) * 0.2);
  height: calc(var(--s) * 0.2);
  background: color-mix(in srgb, var(--ldr-color, #3b82f6) 60%, transparent);
  animation-direction: alternate-reverse;
}
@keyframes ldr-pulley-rope {
  from { transform: scaleY(0.4167); }
  to   { transform: scaleY(1); }
}
@keyframes ldr-pulley-lift {
  from { transform: translateY(calc(var(--s) * 0.25)); }
  to   { transform: translateY(calc(var(--s) * 0.6)); }
}
/* 0.35 --s of rope over a 0.25 --s radius wheel is 80 degrees. */
@keyframes ldr-pulley-turn {
  from { transform: rotate(0deg); }
  to   { transform: rotate(-80deg); }
}

@media (prefers-reduced-motion: reduce) {
  .ldr-pulley, .ldr-pulley *, .ldr-pulley::before, .ldr-pulley::after, .ldr-pulley *::before, .ldr-pulley *::after {
    animation-duration: 3s;
  }
}

Customizing this loader

  • Color: set --ldr-color on the element or a parent. Currently previewing #3b82f6.
  • Size: set --ldr-size. Every dimension in the snippet is derived from it. Currently previewing 64px.
  • Speed: change the duration on the animation shorthand. Delays on the children are relative, so they keep their rhythm.
  • Accessibility: the root carries 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.

Similar loaders