A dot with a two-dot trail loops an infinity track. Two nested ease-in-out sine swings, one sideways and one twice as fast up and down, trace the curve with no path data at all.
Also known as: infinity path animation css, figure 8 motion css, lissajous animation css.
<div class="kfa-figure-eight"><svg class="kfa-figure-eight-track" viewBox="-6 -2.5 12 5" aria-hidden="true"><path d="M-4.60 0.00L-4.56 0.39L-4.44 0.75L-4.25 1.06L-3.98 1.30L-3.65 1.45L-3.25 1.50L-2.80 1.45L-2.30 1.30L-1.76 1.06L-1.19 0.75L-0.60 0.39L-0.00 0.00L0.60 -0.39L1.19 -0.75L1.76 -1.06L2.30 -1.30L2.80 -1.45L3.25 -1.50L3.65 -1.45L3.98 -1.30L4.25 -1.06L4.44 -0.75L4.56 -0.39L4.60 -0.00L4.56 0.39L4.44 0.75L4.25 1.06L3.98 1.30L3.65 1.45L3.25 1.50L2.80 1.45L2.30 1.30L1.76 1.06L1.19 0.75L0.60 0.39L0.00 0.00L-0.60 -0.39L-1.19 -0.75L-1.76 -1.06L-2.30 -1.30L-2.80 -1.45L-3.25 -1.50L-3.65 -1.45L-3.98 -1.30L-4.25 -1.06L-4.44 -0.75L-4.56 -0.39Z" /></svg><span class="kfa-figure-eight-x" style="--i: 2"><span class="kfa-figure-eight-y"></span></span><span class="kfa-figure-eight-x" style="--i: 1"><span class="kfa-figure-eight-y"></span></span><span class="kfa-figure-eight-x" style="--i: 0"><span class="kfa-figure-eight-y"></span></span></div>
.kfa-figure-eight {
--c: var(--kfa-color, #3b82f6);
font-size: var(--kfa-size, 16px);
position: relative;
width: 12em;
height: 5em;
}
.kfa-figure-eight-track {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
fill: none;
stroke: color-mix(in srgb, var(--c) 35%, transparent);
stroke-width: 0.1;
stroke-dasharray: 0.25 0.2;
}
/* sideways swing: one full sweep each way per cycle */
.kfa-figure-eight-x {
position: absolute;
left: 50%;
top: 50%;
animation: kfa-figure-eight-sway 1.6s cubic-bezier(0.37, 0, 0.63, 1) calc(var(--i) * 0.08s - 0.16s) infinite alternate;
}
/* vertical swing at twice the rate, a quarter period ahead: that phase makes the crossing */
.kfa-figure-eight-y {
display: block;
width: 1.2em;
height: 1.2em;
margin: -0.6em;
border-radius: 50%;
background: var(--c);
box-shadow: 0 0 0.8em color-mix(in srgb, var(--c) 55%, transparent);
opacity: calc(1 - var(--i) * 0.35);
scale: calc(1 - var(--i) * 0.2);
animation: kfa-figure-eight-bob 0.8s cubic-bezier(0.37, 0, 0.63, 1) calc(var(--i) * 0.08s - 0.56s) infinite alternate;
}
@keyframes kfa-figure-eight-sway {
from { transform: translateX(-4.6em); }
to { transform: translateX(4.6em); }
}
@keyframes kfa-figure-eight-bob {
from { transform: translateY(-1.5em); }
to { transform: translateY(1.5em); }
}
@media (prefers-reduced-motion: reduce) {
.kfa-figure-eight, .kfa-figure-eight *, .kfa-figure-eight::before, .kfa-figure-eight::after, .kfa-figure-eight *::before, .kfa-figure-eight *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}--kfa-color on the element or a parent.
Currently previewing #3b82f6.--kfa-size. Every other dimension is in
em, so the whole thing scales together. Currently previewing 16px.transition or animation
shorthand. Staggered delays are relative, so they keep their rhythm.