Skip to content
Plinth

Original by Plinth · MIT

Loaders

Wave Bars

Seven bars pulsing on staggered phases, so the row reads as a travelling wave rather than a flicker.

#loader#wave#bars#css#animation

Install

npx shadcn@latest add https://plinthui.com/r/wave-bars.json
Open in v0
// No hooks or handlers, so this stays a server component — it is pure CSS.
// Keyframes and per-child animation delays are outside what Tailwind can
// express, so they live in the style element below.
const css = `
.wave {
  display: flex;
  align-items: center;
  gap: 7px;
  /* Bars scale from their centre, so the row stays optically balanced instead
     of growing downward off the baseline. */
  height: 76px;
}

.wave__bar {
  width: 9px;
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(180deg, #ffca7a, #ff5f9e);
  transform-origin: center;
  /* scaleY rather than height: it composites, so seven of these cost the
     browser nothing, while an animated height relayouts every frame. */
  animation: wave-pulse 1.05s ease-in-out infinite;
}

/* A phase offset per bar is the whole effect — same animation, staggered
   start, and the row reads as a travelling wave rather than a flicker. */
.wave__bar:nth-child(1) { animation-delay: -0.98s; }
.wave__bar:nth-child(2) { animation-delay: -0.84s; }
.wave__bar:nth-child(3) { animation-delay: -0.70s; }
.wave__bar:nth-child(4) { animation-delay: -0.56s; }
.wave__bar:nth-child(5) { animation-delay: -0.42s; }
.wave__bar:nth-child(6) { animation-delay: -0.28s; }
.wave__bar:nth-child(7) { animation-delay: -0.14s; }

@keyframes wave-pulse {
  0%, 100% { transform: scaleY(0.22); opacity: 0.45; }
  50%      { transform: scaleY(1); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .wave__bar { animation: none; transform: scaleY(0.6); opacity: 0.8; }
}
`;

export function WaveBars({ bars = 7 }: { bars?: number }) {
  return (
    <>
      <style>{css}</style>
      {/* One status announcement for the whole group; the bars are decoration. */}
      <div className="wave" role="status" aria-label="Loading">
        {Array.from({ length: bars }, (_, i) => (
          <span className="wave__bar" key={i} />
        ))}
      </div>
    </>
  );
}

More in Loaders

Similar tags