Seen from above, a pendulum swings while its plane slowly turns, knocking over the ring of pins one by one before they pop back up. Pin delays are timed to the exact swing.
Also known as: foucault pendulum animation, pendulum knocking pins, rotating pendulum loader.
<div class="ldr-foucault-pendulum" role="status" aria-label="Loading"> <span class="ldr-foucault-pendulum-pin" style="--k: 0; --t: 3s"></span> <span class="ldr-foucault-pendulum-pin" style="--k: 1; --t: 0.5s"></span> <span class="ldr-foucault-pendulum-pin" style="--k: 2; --t: 4s"></span> <span class="ldr-foucault-pendulum-pin" style="--k: 3; --t: 1.5s"></span> <span class="ldr-foucault-pendulum-pin" style="--k: 4; --t: 5s"></span> <span class="ldr-foucault-pendulum-pin" style="--k: 5; --t: 2.5s"></span> <span class="ldr-foucault-pendulum-pin" style="--k: 6; --t: 0s"></span> <span class="ldr-foucault-pendulum-pin" style="--k: 7; --t: 3.5s"></span> <span class="ldr-foucault-pendulum-pin" style="--k: 8; --t: 1s"></span> <span class="ldr-foucault-pendulum-pin" style="--k: 9; --t: 4.5s"></span> <span class="ldr-foucault-pendulum-pin" style="--k: 10; --t: 2s"></span> <span class="ldr-foucault-pendulum-pin" style="--k: 11; --t: 5.5s"></span> <span class="ldr-foucault-pendulum-plane"><span class="ldr-foucault-pendulum-bob"></span></span> </div>
.ldr-foucault-pendulum {
--s: var(--ldr-size, 48px);
--c: var(--ldr-color, #3b82f6);
position: relative;
width: var(--s);
height: var(--s);
}
.ldr-foucault-pendulum::before {
content: "";
position: absolute;
inset: 0;
box-sizing: border-box;
border: 1px solid color-mix(in srgb, var(--ldr-color, #3b82f6) 30%, transparent);
border-radius: 50%;
}
.ldr-foucault-pendulum-pin {
position: absolute;
top: 50%;
left: 50%;
transform: rotate(calc(var(--k) * 30deg)) translateX(calc(var(--s) * 0.42));
}
.ldr-foucault-pendulum-pin::before {
content: "";
position: absolute;
top: calc(var(--s) / -24);
left: calc(var(--s) / -24);
width: calc(var(--s) / 12);
height: calc(var(--s) / 12);
border-radius: 50%;
background: color-mix(in srgb, var(--ldr-color, #3b82f6) 70%, transparent);
animation: ldr-foucault-pendulum-knock 6s infinite;
animation-delay: var(--t);
}
/* The swing plane turns once every 6s; the bob reaches a pin every half swing. */
.ldr-foucault-pendulum-plane {
position: absolute;
inset: 0;
animation: ldr-foucault-pendulum-precess 6s linear infinite;
}
.ldr-foucault-pendulum-plane::before {
content: "";
position: absolute;
top: 50%;
left: 12%;
right: 12%;
height: 1px;
background: color-mix(in srgb, var(--ldr-color, #3b82f6) 25%, transparent);
}
.ldr-foucault-pendulum-bob {
position: absolute;
top: 50%;
left: 50%;
width: calc(var(--s) / 6);
height: calc(var(--s) / 6);
margin: calc(var(--s) / -12);
border-radius: 50%;
background: var(--c);
animation: ldr-foucault-pendulum-swing 0.5s cubic-bezier(0.37, 0, 0.63, 1) infinite alternate;
}
@keyframes ldr-foucault-pendulum-precess {
to { transform: rotate(360deg); }
}
@keyframes ldr-foucault-pendulum-swing {
from { transform: translateX(calc(var(--s) * -0.295)); }
to { transform: translateX(calc(var(--s) * 0.295)); }
}
@keyframes ldr-foucault-pendulum-knock {
0% { transform: translateX(0) scale(1); opacity: 1; animation-timing-function: ease-out; }
4%, 78% { transform: translateX(calc(var(--s) / 20)) scale(0.5); opacity: 0.3; animation-timing-function: cubic-bezier(0.3, 1.6, 0.6, 1); }
88%, 100% { transform: translateX(0) scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.ldr-foucault-pendulum, .ldr-foucault-pendulum *, .ldr-foucault-pendulum::before, .ldr-foucault-pendulum::after, .ldr-foucault-pendulum *::before, .ldr-foucault-pendulum *::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.