Web InnoventixFreeCode

Bar Striped Progress

Original · free

animated striped progress bar

byWeb InnoventixReact + Tailwind
progbarstripedprogress
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-bar-striped.json
prog-bar-striped.tsx
"use client";

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

type Status = "idle" | "running" | "paused" | "done";

type Task = {
  id: string;
  label: string;
  detail: string;
  rate: number;
};

const TASKS: Task[] = [
  { id: "media", label: "Media library", detail: "2.4 GB · 1,208 files", rate: 0.9 },
  { id: "db", label: "Database snapshot", detail: "860 MB · 1 archive", rate: 1.5 },
  { id: "config", label: "Environment config", detail: "12 MB · 44 keys", rate: 2.6 },
  { id: "search", label: "Search index", detail: "540 MB · 6 shards", rate: 1.15 },
];

const SPEEDS = [0.5, 1, 2];
const TICK_MS = 120;
const AVG_RATE = TASKS.reduce((a, t) => a + t.rate, 0) / TASKS.length;

function taskState(status: Status, value: number): string {
  if (value >= 100) return "Complete";
  if (status === "idle") return "Queued";
  if (status === "paused") return "Paused";
  if (value <= 0) return "Queued";
  return "Transferring";
}

export default function StripedProgressSync() {
  const prefersReduced = useReducedMotion();

  const [progress, setProgress] = useState<number[]>(() => TASKS.map(() => 0));
  const [status, setStatus] = useState<Status>("idle");
  const [speed, setSpeed] = useState<number>(1);
  const [striped, setStriped] = useState<boolean>(true);

  const speedRefs = useRef<Array<HTMLButtonElement | null>>([]);

  useEffect(() => {
    if (status !== "running") return;
    const id = window.setInterval(() => {
      setProgress((prev) =>
        prev.map((v, i) => {
          if (v >= 100) return 100;
          const step = TASKS[i].rate * speed * (0.6 + Math.random() * 0.8);
          return Math.min(100, v + step);
        }),
      );
    }, TICK_MS);
    return () => window.clearInterval(id);
  }, [status, speed]);

  useEffect(() => {
    if (status === "running" && progress.every((v) => v >= 100)) {
      setStatus("done");
    }
  }, [progress, status]);

  const overall = progress.reduce((a, b) => a + b, 0) / progress.length;
  const overallPct = Math.round(overall);
  const animating = striped && status === "running";

  const etaTicks = overall >= 100 ? 0 : (100 - overall) / (AVG_RATE * speed);
  const etaSeconds = Math.max(0, Math.ceil((etaTicks * TICK_MS) / 1000));

  const primaryLabel =
    status === "running"
      ? "Pause"
      : status === "paused"
        ? "Resume"
        : status === "done"
          ? "Completed"
          : "Start sync";

  const statusMessage =
    status === "idle"
      ? "Ready to sync"
      : status === "running"
        ? "Sync in progress"
        : status === "paused"
          ? "Sync paused"
          : "Sync complete";

  const togglePrimary = () => {
    setStatus((s) => {
      if (s === "running") return "paused";
      if (s === "done") return "done";
      return "running";
    });
  };

  const reset = () => {
    setProgress(TASKS.map(() => 0));
    setStatus("idle");
  };

  const handleSpeedKey = (e: KeyboardEvent<HTMLDivElement>) => {
    const idx = SPEEDS.indexOf(speed);
    let ni = idx;
    if (e.key === "ArrowRight" || e.key === "ArrowDown") ni = (idx + 1) % SPEEDS.length;
    else if (e.key === "ArrowLeft" || e.key === "ArrowUp")
      ni = (idx - 1 + SPEEDS.length) % SPEEDS.length;
    else return;
    e.preventDefault();
    setSpeed(SPEEDS[ni]);
    speedRefs.current[ni]?.focus();
  };

  const rise = (delay: number) =>
    prefersReduced
      ? { initial: false as const }
      : {
          initial: { opacity: 0, y: 14 },
          animate: { opacity: 1, y: 0 },
          transition: { duration: 0.5, delay, ease: [0.22, 1, 0.36, 1] as const },
        };

  const btnBase =
    "inline-flex items-center justify-center gap-2 rounded-xl text-sm font-semibold transition-colors 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-neutral-900 disabled:cursor-not-allowed disabled:opacity-50";

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 sm:py-24 dark:bg-neutral-950 dark:text-slate-100">
      <style>{`
        .progbarstriped-stripes {
          background-image: repeating-linear-gradient(
            45deg,
            rgba(255, 255, 255, 0.24) 0,
            rgba(255, 255, 255, 0.24) 9px,
            transparent 9px,
            transparent 18px
          );
          background-size: 25.46px 25.46px;
        }
        .progbarstriped-stripes[data-animate="true"] {
          animation: progbarstriped-slide 0.75s linear infinite;
        }
        @keyframes progbarstriped-slide {
          from { background-position: 0 0; }
          to { background-position: 25.46px 0; }
        }
        .progbarstriped-glow[data-animate="true"] {
          animation: progbarstriped-pulse 2.4s ease-in-out infinite;
        }
        @keyframes progbarstriped-pulse {
          0%, 100% { opacity: 0.35; }
          50% { opacity: 0.75; }
        }
        @media (prefers-reduced-motion: reduce) {
          .progbarstriped-stripes[data-animate="true"],
          .progbarstriped-glow[data-animate="true"] {
            animation: none;
          }
        }
      `}</style>

      <span className="sr-only" aria-live="polite">
        {statusMessage}
      </span>

      <motion.div
        {...rise(0)}
        className="mx-auto w-full max-w-2xl rounded-3xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8 dark:border-neutral-800 dark:bg-neutral-900"
      >
        <div className="flex flex-wrap items-start justify-between gap-4">
          <div>
            <div className="flex items-center gap-2">
              <span
                className={
                  "inline-flex h-2.5 w-2.5 rounded-full " +
                  (status === "done"
                    ? "bg-emerald-500"
                    : status === "running"
                      ? "bg-indigo-500"
                      : status === "paused"
                        ? "bg-amber-500"
                        : "bg-slate-400 dark:bg-neutral-600")
                }
                aria-hidden="true"
              />
              <p className="text-xs font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400">
                {statusMessage}
              </p>
            </div>
            <h2 className="mt-2 text-xl font-bold tracking-tight sm:text-2xl">
              Syncing workspace to edge
            </h2>
            <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
              3.8 GB across {TASKS.length} tasks · region <span className="font-medium text-slate-700 dark:text-slate-300">fra-1</span>
            </p>
          </div>

          <div className="text-right">
            <div className="text-4xl font-bold tabular-nums tracking-tight sm:text-5xl">
              {overallPct}
              <span className="text-2xl text-slate-400 dark:text-slate-500">%</span>
            </div>
            <p className="mt-1 text-xs text-slate-500 tabular-nums dark:text-slate-400">
              {status === "done"
                ? "Finished"
                : status === "running"
                  ? `~${etaSeconds}s remaining`
                  : status === "paused"
                    ? "Paused"
                    : "Not started"}
            </p>
          </div>
        </div>

        <div className="mt-6">
          <div
            className="relative h-4 w-full overflow-hidden rounded-full bg-slate-200 sm:h-5 dark:bg-neutral-800"
            role="progressbar"
            aria-label="Overall sync progress"
            aria-valuenow={overallPct}
            aria-valuemin={0}
            aria-valuemax={100}
          >
            <div
              className={
                "relative h-full rounded-full bg-gradient-to-r transition-[width] duration-200 ease-out " +
                (status === "done"
                  ? "from-emerald-500 to-emerald-400"
                  : "from-indigo-600 via-violet-500 to-indigo-500")
              }
              style={{ width: `${overall}%` }}
            >
              <div
                className="progbarstriped-stripes absolute inset-0 rounded-full"
                data-animate={animating ? "true" : "false"}
                aria-hidden="true"
              />
              <div
                className="progbarstriped-glow absolute inset-y-0 right-0 w-8 rounded-full bg-white/50 blur-md"
                data-animate={animating ? "true" : "false"}
                aria-hidden="true"
              />
            </div>
          </div>
        </div>

        <ul className="mt-8 space-y-5">
          {TASKS.map((task, i) => {
            const value = progress[i];
            const pct = Math.round(value);
            const done = value >= 100;
            const label = taskState(status, value);
            return (
              <motion.li key={task.id} {...rise(0.08 + i * 0.06)}>
                <div className="flex items-baseline justify-between gap-4">
                  <div className="min-w-0">
                    <p className="truncate text-sm font-semibold">{task.label}</p>
                    <p className="truncate text-xs text-slate-500 dark:text-slate-400">
                      {task.detail}
                    </p>
                  </div>
                  <div className="flex shrink-0 items-center gap-2">
                    <span
                      className={
                        "rounded-full px-2 py-0.5 text-[11px] font-medium " +
                        (done
                          ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300"
                          : label === "Transferring"
                            ? "bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300"
                            : label === "Paused"
                              ? "bg-amber-100 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300"
                              : "bg-slate-100 text-slate-600 dark:bg-neutral-800 dark:text-slate-400")
                      }
                    >
                      {label}
                    </span>
                    <span className="w-10 text-right text-sm font-semibold tabular-nums text-slate-600 dark:text-slate-300">
                      {pct}%
                    </span>
                  </div>
                </div>
                <div
                  className="relative mt-2 h-2.5 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-neutral-800"
                  role="progressbar"
                  aria-label={`${task.label} progress`}
                  aria-valuenow={pct}
                  aria-valuemin={0}
                  aria-valuemax={100}
                >
                  <div
                    className={
                      "relative h-full rounded-full bg-gradient-to-r transition-[width] duration-200 ease-out " +
                      (done
                        ? "from-emerald-500 to-emerald-400"
                        : "from-indigo-500 to-violet-500")
                    }
                    style={{ width: `${value}%` }}
                  >
                    <div
                      className="progbarstriped-stripes absolute inset-0 rounded-full"
                      data-animate={striped && status === "running" && !done ? "true" : "false"}
                      aria-hidden="true"
                    />
                  </div>
                </div>
              </motion.li>
            );
          })}
        </ul>

        <motion.div
          {...rise(0.36)}
          className="mt-8 flex flex-wrap items-center gap-3 border-t border-slate-200 pt-6 dark:border-neutral-800"
        >
          <button
            type="button"
            onClick={togglePrimary}
            disabled={status === "done"}
            aria-label={primaryLabel}
            className={
              btnBase +
              " h-10 px-4 text-white " +
              (status === "running"
                ? "bg-amber-600 hover:bg-amber-500"
                : "bg-indigo-600 hover:bg-indigo-500")
            }
          >
            {status === "running" ? (
              <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.53.85l10.79-6.86a1 1 0 0 0 0-1.7L9.53 4.29A1 1 0 0 0 8 5.14Z" />
              </svg>
            )}
            {primaryLabel}
          </button>

          <button
            type="button"
            onClick={reset}
            disabled={status === "idle"}
            aria-label="Reset sync"
            className={
              btnBase +
              " h-10 border border-slate-300 bg-white px-4 text-slate-700 hover:bg-slate-100 dark:border-neutral-700 dark:bg-neutral-900 dark:text-slate-200 dark:hover:bg-neutral-800"
            }
          >
            <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
              <path d="M3 3v5h5" />
            </svg>
            Reset
          </button>

          <div className="ml-auto flex items-center gap-4">
            <div className="flex items-center gap-2">
              <span
                id="progbarstriped-speed-label"
                className="text-xs font-medium text-slate-500 dark:text-slate-400"
              >
                Speed
              </span>
              <div
                role="radiogroup"
                aria-labelledby="progbarstriped-speed-label"
                onKeyDown={handleSpeedKey}
                className="flex rounded-xl border border-slate-300 bg-slate-100 p-0.5 dark:border-neutral-700 dark:bg-neutral-800"
              >
                {SPEEDS.map((s, i) => {
                  const active = speed === s;
                  return (
                    <button
                      key={s}
                      ref={(el) => {
                        speedRefs.current[i] = el;
                      }}
                      type="button"
                      role="radio"
                      aria-checked={active}
                      tabIndex={active ? 0 : -1}
                      onClick={() => setSpeed(s)}
                      className={
                        "rounded-lg px-2.5 py-1 text-xs font-semibold tabular-nums transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 focus-visible:ring-offset-slate-100 dark:focus-visible:ring-offset-neutral-800 " +
                        (active
                          ? "bg-white text-indigo-700 shadow-sm dark:bg-neutral-950 dark:text-indigo-300"
                          : "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200")
                      }
                    >
                      {s}×
                    </button>
                  );
                })}
              </div>
            </div>

            <button
              type="button"
              role="switch"
              aria-checked={striped}
              onClick={() => setStriped((v) => !v)}
              className={
                "inline-flex items-center gap-2 rounded-full text-xs font-medium text-slate-500 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-400 dark:focus-visible:ring-offset-neutral-900"
              }
            >
              <span
                className={
                  "relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors " +
                  (striped ? "bg-indigo-600" : "bg-slate-300 dark:bg-neutral-700")
                }
                aria-hidden="true"
              >
                <span
                  className={
                    "inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform " +
                    (striped ? "translate-x-4" : "translate-x-0.5")
                  }
                />
              </span>
              Stripes
            </button>
          </div>
        </motion.div>
      </motion.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 →