Web InnoventixFreeCode

Indeterminate Progress

Original · free

indeterminate progress bar

byWeb InnoventixReact + Tailwind
progindeterminateprogress
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/prog-indeterminate.json
prog-indeterminate.tsx
"use client";

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

type Variant = "flow" | "stripe" | "bounce";

const STYLES: { id: Variant; label: string }[] = [
  { id: "flow", label: "Flow" },
  { id: "stripe", label: "Stripes" },
  { id: "bounce", label: "Bounce" },
];

const SPEEDS = [0.5, 0.75, 1, 1.5, 2] as const;

type Task = {
  id: string;
  name: string;
  meta: string;
  gradient: string;
};

const TASKS: Task[] = [
  {
    id: "compile",
    name: "Compiling TypeScript",
    meta: "apps/web · 412 modules",
    gradient: "from-indigo-500 to-violet-500",
  },
  {
    id: "images",
    name: "Optimizing images",
    meta: "public/media · 88 assets",
    gradient: "from-emerald-500 to-teal-500",
  },
  {
    id: "upload",
    name: "Uploading artifacts",
    meta: "edge-cdn · us-east-1",
    gradient: "from-amber-500 to-orange-500",
  },
];

function PlayIcon() {
  return (
    <svg viewBox="0 0 20 20" aria-hidden="true" className="h-4 w-4 fill-current">
      <path d="M6.5 4.3a1 1 0 0 1 1.53-.85l7.2 4.7a1 1 0 0 1 0 1.7l-7.2 4.7a1 1 0 0 1-1.53-.85V4.3Z" />
    </svg>
  );
}

function PauseIcon() {
  return (
    <svg viewBox="0 0 20 20" aria-hidden="true" className="h-4 w-4 fill-current">
      <path d="M6 3.5A1.5 1.5 0 0 1 7.5 5v10a1.5 1.5 0 0 1-3 0V5A1.5 1.5 0 0 1 6 3.5Zm8 0A1.5 1.5 0 0 1 15.5 5v10a1.5 1.5 0 0 1-3 0V5A1.5 1.5 0 0 1 14 3.5Z" />
    </svg>
  );
}

function ResetIcon() {
  return (
    <svg viewBox="0 0 20 20" aria-hidden="true" className="h-4 w-4 fill-current">
      <path d="M10 3.5a6.5 6.5 0 1 0 6.32 8.03 1 1 0 1 0-1.94-.47A4.5 4.5 0 1 1 10 5.5c1.05 0 2.02.36 2.79.96l-1.5 1.5a.6.6 0 0 0 .43 1.03H16a.6.6 0 0 0 .6-.6V4.13a.6.6 0 0 0-1.03-.42l-1.16 1.15A6.47 6.47 0 0 0 10 3.5Z" />
    </svg>
  );
}

const STRIPE_IMAGE =
  "repeating-linear-gradient(45deg, rgba(255,255,255,0.28) 0, rgba(255,255,255,0.28) 10px, rgba(255,255,255,0) 10px, rgba(255,255,255,0) 20px)";

function IndeterminateBar({
  variant,
  running,
  reduce,
  gradient,
  label,
  trackClass,
  restartKey,
}: {
  variant: Variant;
  running: boolean;
  reduce: boolean;
  gradient: string;
  label: string;
  trackClass: string;
  restartKey: number;
}) {
  const play: CSSProperties = { animationPlayState: running ? "running" : "paused" };

  return (
    <div
      role="progressbar"
      aria-label={label}
      aria-busy={running}
      aria-valuetext={running ? "In progress, time remaining unknown" : "Paused"}
      className={`relative w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800 ${trackClass}`}
    >
      {reduce ? (
        <>
          <span className={`absolute inset-0 bg-gradient-to-r opacity-80 ${gradient}`} />
          <span className="absolute inset-0" style={{ backgroundImage: STRIPE_IMAGE }} />
        </>
      ) : variant === "flow" ? (
        <div key={restartKey} className="absolute inset-0">
          <span
            className={`progind-flow-a absolute top-0 bottom-0 rounded-full bg-gradient-to-r ${gradient}`}
            style={play}
          />
          <span
            className={`progind-flow-b absolute top-0 bottom-0 rounded-full bg-gradient-to-r ${gradient}`}
            style={play}
          />
        </div>
      ) : variant === "stripe" ? (
        <div key={restartKey} className="absolute inset-0">
          <span className={`absolute inset-0 bg-gradient-to-r ${gradient}`} />
          <span
            className="progind-stripe-fill absolute inset-0"
            style={{ ...play, backgroundImage: STRIPE_IMAGE }}
          />
        </div>
      ) : (
        <div key={restartKey} className="absolute inset-0">
          <span
            className={`progind-bounce-chunk absolute top-0 bottom-0 w-[28%] rounded-full bg-gradient-to-r ${gradient}`}
            style={play}
          />
        </div>
      )}
    </div>
  );
}

export default function ProgIndeterminate() {
  const prefersReduced = useReducedMotion();
  const reduce = prefersReduced === true;

  const [running, setRunning] = useState(true);
  const [variant, setVariant] = useState<Variant>("flow");
  const [speed, setSpeed] = useState(1);
  const [elapsed, setElapsed] = useState(0);
  const [restartKey, setRestartKey] = useState(0);

  useEffect(() => {
    if (!running) return;
    const id = window.setInterval(() => setElapsed((e) => e + 1), 1000);
    return () => window.clearInterval(id);
  }, [running]);

  const duration = (2.1 / speed).toFixed(2) + "s";
  const shellStyle = { "--pd": duration } as CSSProperties;

  const mm = String(Math.floor(elapsed / 60)).padStart(2, "0");
  const ss = String(elapsed % 60).padStart(2, "0");

  function reset() {
    setElapsed(0);
    setRunning(true);
    setRestartKey((k) => k + 1);
  }

  return (
    <section className="relative w-full bg-slate-50 px-4 py-16 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:py-24">
      <style>{`
        @keyframes progind-bar1 {
          0% { left: -35%; right: 100%; }
          60% { left: 100%; right: -90%; }
          100% { left: 100%; right: -90%; }
        }
        @keyframes progind-bar2 {
          0% { left: -200%; right: 100%; }
          60% { left: 107%; right: -8%; }
          100% { left: 107%; right: -8%; }
        }
        @keyframes progind-stripe-move {
          from { background-position: 0 0; }
          to { background-position: 28.28px 0; }
        }
        @keyframes progind-bounce-move {
          from { transform: translateX(0); }
          to { transform: translateX(257%); }
        }
        .progind-flow-a {
          animation: progind-bar1 var(--pd, 2.1s) cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
          will-change: left, right;
        }
        .progind-flow-b {
          animation: progind-bar2 var(--pd, 2.1s) cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
          animation-delay: calc(var(--pd, 2.1s) * -0.6);
          will-change: left, right;
        }
        .progind-stripe-fill {
          animation: progind-stripe-move var(--pd, 2.1s) linear infinite;
          will-change: background-position;
        }
        .progind-bounce-chunk {
          animation: progind-bounce-move var(--pd, 2.1s) cubic-bezier(0.45, 0, 0.55, 1) infinite alternate;
          will-change: transform;
        }
        @media (prefers-reduced-motion: reduce) {
          .progind-flow-a, .progind-flow-b, .progind-stripe-fill, .progind-bounce-chunk {
            animation: none !important;
          }
        }
      `}</style>

      <div
        style={shellStyle}
        className="mx-auto flex max-w-2xl flex-col gap-6"
      >
        <header className="flex flex-col gap-3">
          <span className="inline-flex w-fit items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-semibold uppercase tracking-wide text-indigo-600 dark:border-slate-800 dark:bg-slate-900 dark:text-indigo-400">
            <span
              className={`h-2 w-2 rounded-full ${
                running ? "bg-emerald-500" : "bg-slate-400 dark:bg-slate-600"
              } ${running && !reduce ? "animate-pulse" : ""}`}
            />
            Pipeline status
          </span>
          <h2 className="text-2xl font-semibold tracking-tight sm:text-3xl">
            Deploying to production
          </h2>
          <p className="max-w-prose text-sm text-slate-600 dark:text-slate-400">
            This step doesn&apos;t report a percentage — we can&apos;t know how long it
            takes. The bar loops to signal work is happening until the build resolves.
          </p>
        </header>

        <div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900 sm:p-6">
          <div className="mb-3 flex items-center justify-between text-sm">
            <span className="font-medium text-slate-700 dark:text-slate-200">
              {running ? "Building bundle…" : "Paused"}
            </span>
            <span className="tabular-nums font-medium text-slate-500 dark:text-slate-400">
              Elapsed {mm}:{ss}
            </span>
          </div>

          <IndeterminateBar
            variant={variant}
            running={running}
            reduce={reduce}
            gradient="from-indigo-500 via-violet-500 to-sky-500"
            label="Production build progress"
            trackClass="h-3"
            restartKey={restartKey}
          />

          <p aria-live="polite" className="sr-only">
            {running ? "Build in progress." : "Build paused."}
          </p>

          <div className="mt-5 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
            <div className="flex items-center gap-2">
              <button
                type="button"
                aria-pressed={running}
                onClick={() => setRunning((r) => !r)}
                className="inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-3.5 py-2 text-sm font-semibold text-white transition-colors hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900"
              >
                {running ? <PauseIcon /> : <PlayIcon />}
                {running ? "Pause" : "Resume"}
              </button>
              <button
                type="button"
                onClick={reset}
                className="inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-3.5 py-2 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
              >
                <ResetIcon />
                Reset
              </button>
            </div>

            <fieldset className="flex items-center gap-1 rounded-xl bg-slate-100 p-1 dark:bg-slate-800">
              <legend className="sr-only">Animation style</legend>
              {STYLES.map((s) => (
                <label key={s.id} className="relative flex-1 cursor-pointer">
                  <input
                    type="radio"
                    name="progind-style"
                    value={s.id}
                    checked={variant === s.id}
                    onChange={() => setVariant(s.id)}
                    className="peer sr-only"
                  />
                  <span className="flex select-none items-center justify-center rounded-lg px-3.5 py-1.5 text-sm font-medium text-slate-600 ring-1 ring-inset ring-transparent transition-colors peer-checked:bg-white peer-checked:text-indigo-600 peer-checked:shadow-sm peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 dark:text-slate-300 dark:peer-checked:bg-slate-950 dark:peer-checked:text-indigo-400">
                    {s.label}
                  </span>
                </label>
              ))}
            </fieldset>
          </div>

          <div className="mt-4 flex items-center gap-3">
            <label
              htmlFor="progind-speed"
              className="text-sm font-medium text-slate-600 dark:text-slate-400"
            >
              Speed
            </label>
            <input
              id="progind-speed"
              type="range"
              min={0}
              max={SPEEDS.length - 1}
              step={1}
              value={SPEEDS.indexOf(speed as (typeof SPEEDS)[number])}
              onChange={(e) => setSpeed(SPEEDS[Number(e.target.value)])}
              aria-valuetext={`${speed} times`}
              className="h-1.5 flex-1 cursor-pointer appearance-none rounded-full bg-slate-200 accent-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
            />
            <span className="w-10 text-right text-sm font-semibold tabular-nums text-slate-700 dark:text-slate-200">
              {speed}×
            </span>
          </div>
        </div>

        <div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900 sm:p-6">
          <h3 className="mb-4 text-sm font-semibold text-slate-700 dark:text-slate-200">
            Concurrent steps
          </h3>
          <ul className="flex flex-col gap-4">
            {TASKS.map((t) => (
              <li key={t.id} className="flex flex-col gap-2">
                <div className="flex items-baseline justify-between gap-3">
                  <span className="text-sm font-medium text-slate-800 dark:text-slate-100">
                    {t.name}
                  </span>
                  <span className="shrink-0 text-xs text-slate-500 dark:text-slate-400">
                    {t.meta}
                  </span>
                </div>
                <IndeterminateBar
                  variant="flow"
                  running={running}
                  reduce={reduce}
                  gradient={t.gradient}
                  label={`${t.name} progress`}
                  trackClass="h-1.5"
                  restartKey={restartKey}
                />
              </li>
            ))}
          </ul>
        </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 →