Web InnoventixFreeCode

Bars Loader

Original · free

equalizer bars loader

byWeb InnoventixReact + Tailwind
loadbarsloaders
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/load-bars.json
load-bars.tsx
"use client";

import { useMemo, useState, type CSSProperties } from "react";
import { motion, useReducedMotion } from "motion/react";

type Accent = {
  id: string;
  label: string;
  bar: string;
  swatch: string;
  solid: string;
  ring: string;
  accentColor: string;
  glow: string;
};

const ACCENTS: Accent[] = [
  {
    id: "indigo",
    label: "Indigo",
    bar: "from-indigo-500 to-indigo-300 dark:from-indigo-400 dark:to-indigo-200",
    swatch: "bg-indigo-500",
    solid: "bg-indigo-600 text-white hover:bg-indigo-500 active:bg-indigo-700",
    ring: "focus-visible:ring-indigo-500",
    accentColor: "accent-indigo-500",
    glow: "bg-indigo-400/30 dark:bg-indigo-500/25",
  },
  {
    id: "violet",
    label: "Violet",
    bar: "from-violet-500 to-violet-300 dark:from-violet-400 dark:to-violet-200",
    swatch: "bg-violet-500",
    solid: "bg-violet-600 text-white hover:bg-violet-500 active:bg-violet-700",
    ring: "focus-visible:ring-violet-500",
    accentColor: "accent-violet-500",
    glow: "bg-violet-400/30 dark:bg-violet-500/25",
  },
  {
    id: "sky",
    label: "Sky",
    bar: "from-sky-500 to-sky-300 dark:from-sky-400 dark:to-sky-200",
    swatch: "bg-sky-500",
    solid: "bg-sky-600 text-white hover:bg-sky-500 active:bg-sky-700",
    ring: "focus-visible:ring-sky-500",
    accentColor: "accent-sky-500",
    glow: "bg-sky-400/30 dark:bg-sky-500/25",
  },
  {
    id: "emerald",
    label: "Emerald",
    bar: "from-emerald-500 to-emerald-300 dark:from-emerald-400 dark:to-emerald-200",
    swatch: "bg-emerald-500",
    solid: "bg-emerald-600 text-white hover:bg-emerald-500 active:bg-emerald-700",
    ring: "focus-visible:ring-emerald-500",
    accentColor: "accent-emerald-500",
    glow: "bg-emerald-400/30 dark:bg-emerald-500/25",
  },
  {
    id: "amber",
    label: "Amber",
    bar: "from-amber-500 to-amber-300 dark:from-amber-400 dark:to-amber-200",
    swatch: "bg-amber-500",
    solid:
      "bg-amber-500 text-neutral-900 hover:bg-amber-400 active:bg-amber-600",
    ring: "focus-visible:ring-amber-500",
    accentColor: "accent-amber-500",
    glow: "bg-amber-400/30 dark:bg-amber-500/25",
  },
  {
    id: "rose",
    label: "Rose",
    bar: "from-rose-500 to-rose-300 dark:from-rose-400 dark:to-rose-200",
    swatch: "bg-rose-500",
    solid: "bg-rose-600 text-white hover:bg-rose-500 active:bg-rose-700",
    ring: "focus-visible:ring-rose-500",
    accentColor: "accent-rose-500",
    glow: "bg-rose-400/30 dark:bg-rose-500/25",
  },
];

const DENSITIES = [12, 18, 24, 32] as const;

/** Deterministic 0..1 pseudo-random so server and client render identically. */
function seededUnit(i: number): number {
  const x = Math.sin(i * 12.9898 + 4.137) * 43758.5453;
  return x - Math.floor(x);
}

export default function EqualizerBarsLoader() {
  const reduce = useReducedMotion();
  const [playing, setPlaying] = useState(true);
  const [speed, setSpeed] = useState(1.4);
  const [count, setCount] = useState<number>(24);
  const [accentId, setAccentId] = useState<string>("indigo");

  const accent = ACCENTS.find((a) => a.id === accentId) ?? ACCENTS[0];

  const bars = useMemo(() => {
    return Array.from({ length: count }, (_, i) => {
      const p = seededUnit(i);
      const q = seededUnit(i + 101);
      const r = seededUnit(i + 211);
      const peak = 0.32 + p * 0.63;
      const duration = (720 + q * 620) / Math.max(speed, 0.1);
      const delay = -(r * 900);
      return { peak, duration, delay };
    });
  }, [count, speed]);

  const running = playing && !reduce;

  const statusText = reduce
    ? "Reduced motion is on. The loader is shown as a static waveform."
    : playing
      ? `Loader running at ${speed.toFixed(1)}× speed with ${count} bars.`
      : "Loader paused.";

  const focusRing =
    "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-neutral-950";

  return (
    <section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 to-white px-4 py-20 text-neutral-900 dark:from-neutral-950 dark:to-neutral-900 dark:text-neutral-100 sm:px-6 lg:py-28">
      <style>{`
        @keyframes eqbars-pulse {
          0%   { transform: scaleY(0.16); opacity: 0.55; }
          100% { transform: scaleY(1);    opacity: 1; }
        }
        @media (prefers-reduced-motion: reduce) {
          .eqbars-bar { animation: none !important; }
        }
      `}</style>

      <div className="mx-auto max-w-4xl">
        <motion.div
          initial={reduce ? false : { opacity: 0, y: 16 }}
          whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
          viewport={{ once: true, amount: 0.4 }}
          transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
        >
          <header className="mb-8 text-center">
            <p className="mb-3 inline-flex items-center gap-2 rounded-full border border-neutral-200 bg-white/70 px-3 py-1 text-xs font-medium uppercase tracking-widest text-neutral-500 dark:border-neutral-800 dark:bg-neutral-900/70 dark:text-neutral-400">
              <span
                className={`inline-block h-1.5 w-1.5 rounded-full ${accent.swatch}`}
                aria-hidden="true"
              />
              Loaders / Equalizer
            </p>
            <h2 className="text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
              Sound-wave loading indicator
            </h2>
            <p className="mx-auto mt-3 max-w-xl text-pretty text-sm leading-relaxed text-neutral-600 dark:text-neutral-400 sm:text-base">
              An indeterminate loader that reads like a live audio equalizer.
              Tune the tempo, density, and colour, then drop it into any waiting
              state.
            </p>
          </header>

          <div className="overflow-hidden rounded-3xl border border-neutral-200 bg-white/80 shadow-xl shadow-neutral-900/5 backdrop-blur dark:border-neutral-800 dark:bg-neutral-900/70 dark:shadow-black/30">
            {/* Stage */}
            <div className="relative flex flex-col items-center justify-center px-6 pb-8 pt-12 sm:px-12">
              <div
                className={`pointer-events-none absolute inset-x-0 top-1/3 mx-auto h-40 max-w-md rounded-full blur-3xl transition-colors duration-500 ${accent.glow}`}
                aria-hidden="true"
              />

              <div
                role="img"
                aria-label={`Equalizer-style loading animation with ${count} bars, currently ${
                  running ? "animating" : "paused"
                }.`}
                className="relative flex h-40 w-full max-w-xl items-end justify-center gap-[3px] sm:gap-1.5"
              >
                {bars.map((b, i) => {
                  const style: CSSProperties = {
                    height: `${(b.peak * 100).toFixed(2)}%`,
                    transformOrigin: "bottom",
                    animationName: reduce ? undefined : "eqbars-pulse",
                    animationDuration: `${b.duration.toFixed(0)}ms`,
                    animationDelay: `${b.delay.toFixed(0)}ms`,
                    animationTimingFunction: "cubic-bezier(0.36, 0, 0.66, 1)",
                    animationIterationCount: "infinite",
                    animationDirection: "alternate",
                    animationPlayState: running ? "running" : "paused",
                  };
                  return (
                    <span
                      key={i}
                      className={`eqbars-bar block min-w-[2px] flex-1 rounded-full bg-gradient-to-t ${accent.bar}`}
                      style={style}
                    />
                  );
                })}
              </div>

              <div
                className="mt-6 h-px w-full max-w-xl bg-gradient-to-r from-transparent via-neutral-300 to-transparent dark:via-neutral-700"
                aria-hidden="true"
              />

              <p className="mt-4 flex items-center gap-2 text-sm font-medium text-neutral-600 dark:text-neutral-300">
                <span
                  className={`relative inline-flex h-2 w-2 ${
                    running ? "" : "opacity-40"
                  }`}
                  aria-hidden="true"
                >
                  {running && (
                    <span
                      className={`absolute inset-0 animate-ping rounded-full ${accent.swatch} opacity-75`}
                    />
                  )}
                  <span
                    className={`relative inline-flex h-2 w-2 rounded-full ${accent.swatch}`}
                  />
                </span>
                {reduce
                  ? "Waveform (reduced motion)"
                  : playing
                    ? "Streaming…"
                    : "Paused"}
              </p>

              <p aria-live="polite" role="status" className="sr-only">
                {statusText}
              </p>
            </div>

            {/* Controls */}
            <div className="grid grid-cols-1 gap-px border-t border-neutral-200 bg-neutral-200 dark:border-neutral-800 dark:bg-neutral-800 sm:grid-cols-2">
              {/* Playback */}
              <fieldset className="bg-white p-6 dark:bg-neutral-900">
                <legend className="mb-3 text-xs font-semibold uppercase tracking-widest text-neutral-500 dark:text-neutral-400">
                  Playback
                </legend>
                <button
                  type="button"
                  onClick={() => setPlaying((p) => !p)}
                  aria-pressed={playing}
                  className={`inline-flex w-full items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-semibold shadow-sm transition ${accent.solid} ${accent.ring} ${focusRing}`}
                >
                  {playing ? (
                    <svg
                      viewBox="0 0 24 24"
                      className="h-4 w-4"
                      fill="currentColor"
                      aria-hidden="true"
                    >
                      <rect x="6" y="5" width="4" height="14" rx="1" />
                      <rect x="14" y="5" width="4" height="14" rx="1" />
                    </svg>
                  ) : (
                    <svg
                      viewBox="0 0 24 24"
                      className="h-4 w-4"
                      fill="currentColor"
                      aria-hidden="true"
                    >
                      <path d="M8 5.14v13.72a1 1 0 0 0 1.54.84l10.29-6.86a1 1 0 0 0 0-1.68L9.54 4.3A1 1 0 0 0 8 5.14Z" />
                    </svg>
                  )}
                  {playing ? "Pause" : "Play"}
                </button>
                <p className="mt-3 text-xs leading-relaxed text-neutral-500 dark:text-neutral-400">
                  {reduce
                    ? "Your system prefers reduced motion, so the bars hold still."
                    : "Toggle the animation without unmounting the loader."}
                </p>
              </fieldset>

              {/* Tempo */}
              <div className="bg-white p-6 dark:bg-neutral-900">
                <div className="mb-3 flex items-center justify-between">
                  <label
                    htmlFor="eqbars-tempo"
                    className="text-xs font-semibold uppercase tracking-widest text-neutral-500 dark:text-neutral-400"
                  >
                    Tempo
                  </label>
                  <span className="rounded-md bg-neutral-100 px-2 py-0.5 text-xs font-semibold tabular-nums text-neutral-700 dark:bg-neutral-800 dark:text-neutral-200">
                    {speed.toFixed(1)}&times;
                  </span>
                </div>
                <input
                  id="eqbars-tempo"
                  type="range"
                  min={0.5}
                  max={2.5}
                  step={0.1}
                  value={speed}
                  onChange={(e) => setSpeed(Number(e.target.value))}
                  aria-valuetext={`${speed.toFixed(1)} times speed`}
                  className={`h-2 w-full cursor-pointer rounded-full bg-neutral-200 dark:bg-neutral-700 ${accent.accentColor} ${accent.ring} ${focusRing}`}
                />
                <div className="mt-2 flex justify-between text-[11px] font-medium text-neutral-400 dark:text-neutral-500">
                  <span>Slow</span>
                  <span>Fast</span>
                </div>
              </div>

              {/* Density */}
              <div className="bg-white p-6 dark:bg-neutral-900">
                <p
                  id="eqbars-density-label"
                  className="mb-3 text-xs font-semibold uppercase tracking-widest text-neutral-500 dark:text-neutral-400"
                >
                  Density
                </p>
                <div
                  role="radiogroup"
                  aria-labelledby="eqbars-density-label"
                  className="grid grid-cols-4 gap-2"
                >
                  {DENSITIES.map((n) => (
                    <label key={n} className="relative block">
                      <input
                        type="radio"
                        name="eqbars-density"
                        value={n}
                        checked={count === n}
                        onChange={() => setCount(n)}
                        aria-label={`${n} bars`}
                        className="peer sr-only"
                      />
                      <span
                        className={`flex h-10 items-center justify-center rounded-lg border border-neutral-200 bg-white text-sm font-semibold text-neutral-600 transition peer-hover:border-neutral-300 peer-checked:border-transparent peer-checked:bg-neutral-900 peer-checked:text-white peer-focus-visible:ring-2 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-300 dark:peer-checked:bg-white dark:peer-checked:text-neutral-900 dark:peer-focus-visible:ring-offset-neutral-950 ${accent.ring}`}
                      >
                        {n}
                      </span>
                    </label>
                  ))}
                </div>
              </div>

              {/* Colour */}
              <div className="bg-white p-6 dark:bg-neutral-900">
                <p
                  id="eqbars-colour-label"
                  className="mb-3 text-xs font-semibold uppercase tracking-widest text-neutral-500 dark:text-neutral-400"
                >
                  Colour
                </p>
                <div
                  role="radiogroup"
                  aria-labelledby="eqbars-colour-label"
                  className="flex flex-wrap gap-3"
                >
                  {ACCENTS.map((a) => (
                    <label
                      key={a.id}
                      className="relative block"
                      title={a.label}
                    >
                      <input
                        type="radio"
                        name="eqbars-colour"
                        value={a.id}
                        checked={accentId === a.id}
                        onChange={() => setAccentId(a.id)}
                        aria-label={a.label}
                        className="peer sr-only"
                      />
                      <span
                        className={`block h-8 w-8 rounded-full ${a.swatch} ring-offset-2 ring-offset-white transition peer-checked:ring-2 peer-checked:ring-neutral-900 peer-focus-visible:ring-2 peer-focus-visible:ring-sky-500 dark:ring-offset-neutral-900 dark:peer-checked:ring-white`}
                      />
                    </label>
                  ))}
                </div>
              </div>
            </div>
          </div>

          <p className="mt-6 text-center text-xs text-neutral-400 dark:text-neutral-500">
            Pure CSS bars &mdash; no canvas, no images. Honours{" "}
            <code className="rounded bg-neutral-100 px-1 py-0.5 font-mono text-[11px] text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300">
              prefers-reduced-motion
            </code>
            .
          </p>
        </motion.div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →