Web InnoventixFreeCode

Bar Basic Progress

Original · free

basic progress bars in colours

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

import { useCallback, useId, useMemo, useState } from "react";
import { motion, useReducedMotion } from "motion/react";

type ColorKey = "indigo" | "violet" | "emerald" | "sky" | "amber" | "rose";

type Metric = {
  id: string;
  label: string;
  hint: string;
  color: ColorKey;
  value: number;
};

type ColorStyle = {
  fill: string;
  text: string;
  dot: string;
  accent: string;
  ring: string;
};

const COLORS: Record<ColorKey, ColorStyle> = {
  indigo: {
    fill: "bg-indigo-500 dark:bg-indigo-400",
    text: "text-indigo-600 dark:text-indigo-300",
    dot: "bg-indigo-500 dark:bg-indigo-400",
    accent: "accent-indigo-600 dark:accent-indigo-400",
    ring: "focus-visible:ring-indigo-500 dark:focus-visible:ring-indigo-400",
  },
  violet: {
    fill: "bg-violet-500 dark:bg-violet-400",
    text: "text-violet-600 dark:text-violet-300",
    dot: "bg-violet-500 dark:bg-violet-400",
    accent: "accent-violet-600 dark:accent-violet-400",
    ring: "focus-visible:ring-violet-500 dark:focus-visible:ring-violet-400",
  },
  emerald: {
    fill: "bg-emerald-500 dark:bg-emerald-400",
    text: "text-emerald-600 dark:text-emerald-300",
    dot: "bg-emerald-500 dark:bg-emerald-400",
    accent: "accent-emerald-600 dark:accent-emerald-400",
    ring: "focus-visible:ring-emerald-500 dark:focus-visible:ring-emerald-400",
  },
  sky: {
    fill: "bg-sky-500 dark:bg-sky-400",
    text: "text-sky-600 dark:text-sky-300",
    dot: "bg-sky-500 dark:bg-sky-400",
    accent: "accent-sky-600 dark:accent-sky-400",
    ring: "focus-visible:ring-sky-500 dark:focus-visible:ring-sky-400",
  },
  amber: {
    fill: "bg-amber-500 dark:bg-amber-400",
    text: "text-amber-600 dark:text-amber-300",
    dot: "bg-amber-500 dark:bg-amber-400",
    accent: "accent-amber-600 dark:accent-amber-400",
    ring: "focus-visible:ring-amber-500 dark:focus-visible:ring-amber-400",
  },
  rose: {
    fill: "bg-rose-500 dark:bg-rose-400",
    text: "text-rose-600 dark:text-rose-300",
    dot: "bg-rose-500 dark:bg-rose-400",
    accent: "accent-rose-600 dark:accent-rose-400",
    ring: "focus-visible:ring-rose-500 dark:focus-visible:ring-rose-400",
  },
};

const INITIAL: Metric[] = [
  { id: "uploads", label: "Asset uploads", hint: "1,204 / 1,540 files synced", color: "indigo", value: 78 },
  { id: "training", label: "Model fine-tune", hint: "epoch 7 of 9", color: "violet", value: 64 },
  { id: "coverage", label: "Test coverage", hint: "statements + branches", color: "emerald", value: 92 },
  { id: "latency", label: "Latency budget", hint: "p95 within 220 ms target", color: "sky", value: 46 },
  { id: "storage", label: "Storage used", hint: "38.4 GB of 50 GB", color: "amber", value: 77 },
  { id: "errors", label: "Error budget spent", hint: "30-day rolling window", color: "rose", value: 23 },
];

function clamp(n: number): number {
  if (Number.isNaN(n)) return 0;
  return Math.min(100, Math.max(0, Math.round(n)));
}

export default function ProgBarBasic() {
  const reduce = useReducedMotion();
  const uid = useId();
  const [metrics, setMetrics] = useState<Metric[]>(INITIAL);
  const [striped, setStriped] = useState<boolean>(true);

  const setValue = useCallback((id: string, next: number) => {
    setMetrics((prev) =>
      prev.map((m) => (m.id === id ? { ...m, value: clamp(next) } : m)),
    );
  }, []);

  const randomize = useCallback(() => {
    setMetrics((prev) => prev.map((m) => ({ ...m, value: clamp(5 + Math.random() * 95) })));
  }, []);

  const reset = useCallback(() => setMetrics(INITIAL), []);

  const average = useMemo(() => {
    if (metrics.length === 0) return 0;
    const sum = metrics.reduce((acc, m) => acc + m.value, 0);
    return Math.round(sum / metrics.length);
  }, [metrics]);

  const btn =
    "inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-700 shadow-sm transition-colors hover:bg-slate-50 focus: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-900 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-950";

  const stepBtn =
    "inline-flex h-7 w-7 items-center justify-center rounded-md border border-slate-300 bg-white text-slate-600 transition-colors hover:bg-slate-50 hover:text-slate-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-40 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white dark:focus-visible:ring-offset-slate-950";

  return (
    <section className="relative w-full bg-white px-4 py-16 text-slate-900 sm:px-6 sm:py-24 lg:px-8 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes progbarbasic-move {
          from { background-position: 0 0; }
          to { background-position: 1rem 0; }
        }
        .progbarbasic-stripes {
          background-image: linear-gradient(
            45deg,
            rgba(255, 255, 255, 0.28) 25%,
            transparent 25%,
            transparent 50%,
            rgba(255, 255, 255, 0.28) 50%,
            rgba(255, 255, 255, 0.28) 75%,
            transparent 75%,
            transparent
          );
          background-size: 1rem 1rem;
          animation: progbarbasic-move 0.9s linear infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .progbarbasic-stripes { animation: none; }
        }
      `}</style>

      <div className="mx-auto max-w-3xl">
        <div className="flex flex-col gap-6 rounded-2xl border border-slate-200 bg-slate-50/60 p-6 shadow-sm sm:p-8 dark:border-slate-800 dark:bg-slate-900/40">
          {/* Header */}
          <header className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
            <div>
              <p className="text-xs font-semibold uppercase tracking-widest text-indigo-600 dark:text-indigo-400">
                Release 4.2 &middot; staging
              </p>
              <h2 className="mt-1 text-2xl font-semibold tracking-tight sm:text-3xl">
                Deployment readiness
              </h2>
              <p className="mt-2 text-sm text-slate-600 dark:text-slate-400">
                Six live signals as basic colored progress bars. Drag a slider or use the steppers &mdash; every bar updates in place.
              </p>
            </div>

            <div className="flex shrink-0 flex-col items-start gap-1 sm:items-end">
              <span className="text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
                Overall
              </span>
              <span className="text-3xl font-bold tabular-nums text-slate-900 dark:text-white">
                {average}
                <span className="ml-0.5 text-lg font-semibold text-slate-400 dark:text-slate-500">%</span>
              </span>
            </div>
          </header>

          {/* Toolbar */}
          <div className="flex flex-wrap items-center gap-3 border-y border-slate-200 py-4 dark:border-slate-800">
            <button type="button" onClick={randomize} className={btn}>
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4" aria-hidden="true">
                <path d="M16 3h5v5" />
                <path d="M4 20 21 3" />
                <path d="M21 16v5h-5" />
                <path d="m15 15 6 6" />
                <path d="M4 4l5 5" />
              </svg>
              Randomize
            </button>

            <button type="button" onClick={reset} className={btn}>
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4" aria-hidden="true">
                <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
                <path d="M3 3v5h5" />
              </svg>
              Reset
            </button>

            <button
              type="button"
              role="switch"
              aria-checked={striped}
              onClick={() => setStriped((s) => !s)}
              className={`ml-auto inline-flex items-center gap-2.5 rounded-lg px-2 py-1.5 text-sm font-medium text-slate-700 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-200 dark:focus-visible:ring-offset-slate-950`}
            >
              <span
                aria-hidden="true"
                className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
                  striped ? "bg-indigo-500 dark:bg-indigo-400" : "bg-slate-300 dark:bg-slate-700"
                }`}
              >
                <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>
              Animated stripes
            </button>
          </div>

          {/* Bars */}
          <ul className="flex flex-col gap-7">
            {metrics.map((m) => {
              const c = COLORS[m.color];
              const barId = `${uid}-${m.id}`;
              return (
                <li key={m.id}>
                  <div className="flex items-baseline justify-between gap-3">
                    <span className="flex items-center gap-2 text-sm font-semibold text-slate-800 dark:text-slate-100">
                      <span aria-hidden="true" className={`h-2.5 w-2.5 shrink-0 rounded-full ${c.dot}`} />
                      {m.label}
                    </span>
                    <span className={`text-sm font-bold tabular-nums ${c.text}`}>{m.value}%</span>
                  </div>
                  <p className="mt-0.5 pl-[1.125rem] text-xs text-slate-500 dark:text-slate-400">{m.hint}</p>

                  <div
                    role="progressbar"
                    aria-label={m.label}
                    aria-valuenow={m.value}
                    aria-valuemin={0}
                    aria-valuemax={100}
                    aria-valuetext={`${m.value} percent`}
                    className="relative mt-2 h-2.5 w-full overflow-hidden rounded-full bg-slate-200/90 dark:bg-slate-800"
                  >
                    <motion.div
                      className={`relative h-full rounded-full ${c.fill}`}
                      initial={false}
                      animate={{ width: `${m.value}%` }}
                      transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 150, damping: 24 }}
                    >
                      {striped && (
                        <span aria-hidden="true" className="progbarbasic-stripes absolute inset-0 rounded-full" />
                      )}
                    </motion.div>
                  </div>

                  <div className="mt-3 flex items-center gap-3">
                    <label htmlFor={barId} className="sr-only">
                      Set {m.label}
                    </label>
                    <input
                      id={barId}
                      type="range"
                      min={0}
                      max={100}
                      step={1}
                      value={m.value}
                      onChange={(e) => setValue(m.id, Number(e.target.value))}
                      className={`h-1.5 w-full grow cursor-pointer appearance-none rounded-full bg-slate-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-slate-800 dark:focus-visible:ring-offset-slate-950 ${c.accent} ${c.ring}`}
                    />
                    <div className="flex shrink-0 items-center gap-1">
                      <button
                        type="button"
                        onClick={() => setValue(m.id, m.value - 5)}
                        disabled={m.value <= 0}
                        aria-label={`Decrease ${m.label} by 5 percent`}
                        className={stepBtn}
                      >
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" className="h-3.5 w-3.5" aria-hidden="true">
                          <path d="M5 12h14" />
                        </svg>
                      </button>
                      <button
                        type="button"
                        onClick={() => setValue(m.id, m.value + 5)}
                        disabled={m.value >= 100}
                        aria-label={`Increase ${m.label} by 5 percent`}
                        className={stepBtn}
                      >
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" className="h-3.5 w-3.5" aria-hidden="true">
                          <path d="M12 5v14M5 12h14" />
                        </svg>
                      </button>
                    </div>
                  </div>
                </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 →