Loaders
Infinity Trace
A glowing head running a figure-eight on offset-path, trailed by followers strung along the curve by negative animation delays.
Original by Plinth · MIT
Report a problem or suggest an improvementInstall
npx shadcn@latest add https://plinthui.com/r/infinity-trace.json// No hooks or handlers, so this stays a server component — it is pure CSS
// plus an inline SVG path. offset-path is outside what Tailwind can express.
const PATH =
'M 26 55 C 26 24, 76 24, 95 55 C 114 86, 164 86, 164 55 C 164 24, 114 24, 95 55 C 76 86, 26 86, 26 55 Z';
const css = `
.trace {
position: relative;
width: 190px;
height: 110px;
display: grid;
place-items: center;
}
/* The track and the travellers share ONE path string. offset-path moves an
element along a real curve, so the motion is the geometry — there is no
keyframe list approximating a figure-eight, and changing the path changes
the track and the motion together. Hand-animating translate through a
lemniscate means a dozen keyframes that are wrong between every pair. */
.trace__svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
}
.trace__track {
fill: none;
stroke: rgba(255, 255, 255, 0.1);
stroke-width: 3;
stroke-linecap: round;
}
.trace__dot {
position: absolute;
left: 0;
top: 0;
width: 15px;
height: 15px;
margin: -7.5px 0 0 -7.5px;
border-radius: 50%;
offset-path: path("M 26 55 C 26 24, 76 24, 95 55 C 114 86, 164 86, 164 55 C 164 24, 114 24, 95 55 C 76 86, 26 86, 26 55 Z");
/* offset-rotate: 0deg keeps the head upright; without it the element spins
to face the tangent, which for a round glow is invisible work. */
offset-rotate: 0deg;
animation: trace-run 2.8s linear infinite;
}
.trace__dot--head {
background: radial-gradient(circle at 38% 34%, #fff 0 18%, #ff5ab4 42%, #b45bff 100%);
box-shadow: 0 0 16px 4px rgba(255, 90, 180, 0.55);
}
/* Followers are the same element on the same path, started earlier. A negative
delay begins the animation already in progress, so they are strung out along
the curve from the first frame rather than stacking at the origin and
separating over the first cycle. */
.trace__dot--t1 { background: rgba(255, 90, 180, 0.55); width: 11px; height: 11px; margin: -5.5px 0 0 -5.5px; animation-delay: -0.1s; }
.trace__dot--t2 { background: rgba(180, 91, 255, 0.4); width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px; animation-delay: -0.2s; }
.trace__dot--t3 { background: rgba(91, 140, 255, 0.28); width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px; animation-delay: -0.3s; }
@keyframes trace-run {
from { offset-distance: 0%; }
to { offset-distance: 100%; }
}
.trace__label {
position: absolute;
bottom: -4px;
left: 50%;
transform: translateX(-50%);
font: 600 10px/1 system-ui, sans-serif;
letter-spacing: 0.16em;
text-transform: uppercase;
color: rgba(226, 216, 255, 0.55);
}
@media (prefers-reduced-motion: reduce) {
.trace__dot { animation: none; offset-distance: 24%; }
.trace__dot--t1, .trace__dot--t2, .trace__dot--t3 { display: none; }
}
`;
export function InfinityTrace() {
return (
<>
<style>{css}</style>
<div className="trace" role="status" aria-label="Loading">
<svg className="trace__svg" viewBox="0 0 190 110" aria-hidden="true">
<path className="trace__track" d={PATH} />
</svg>
<span className="trace__dot trace__dot--t3" />
<span className="trace__dot trace__dot--t2" />
<span className="trace__dot trace__dot--t1" />
<span className="trace__dot trace__dot--head" />
<span className="trace__label">Loading</span>
</div>
</>
);
}