Skip to content
All Taste components

Data display

Progress

How far along something is, as labelled bars or rings, plus a spinner when the end is unknown.

bar · warning bar · thin bar · ring · small ring · spinner

Storage6.2 of 10 GB
Invoices this month92 of 100
8 left on the Starter plan.
Profile setup3 of 5 steps
72%Collected45%UploadingSyncing
import { cn } from "@/lib/utils";

export function ProgressBar({ label, value, max = 100, detail, tone = "primary" }:
  { label: string; value: number; max?: number; detail?: string; tone?: "primary" | "warning" | "danger" }) {
  const pct = Math.min(100, Math.round((value / max) * 100));
  return (
    <div className="grid gap-1.5">
      <div className="flex justify-between gap-3 text-[13px]">
        <span className="font-medium">{label}</span>
        <span className="tabular-nums text-fg-muted">{detail ?? pct + "%"}</span>
      </div>
      <div role="progressbar" aria-label={label} aria-valuenow={value} aria-valuemin={0} aria-valuemax={max}
        className="h-2 overflow-hidden rounded-full bg-surface-2">
        <div
          className={cn("h-full rounded-full transition-[width] duration-500", { primary: "bg-primary", warning: "bg-warning", danger: "bg-danger" }[tone])}
          style={{ width: pct + "%" }}
        />
      </div>
    </div>
  );
}

export function ProgressRing({ value, size = 72, stroke = 7, label }: { value: number; size?: number; stroke?: number; label: string }) {
  const r = (size - stroke) / 2;
  return (
    <span role="progressbar" aria-label={label} aria-valuenow={value} aria-valuemin={0} aria-valuemax={100}
      className="relative inline-grid place-items-center">
      <svg width={size} height={size} className="-rotate-90" aria-hidden>
        <circle cx={size / 2} cy={size / 2} r={r} fill="none" strokeWidth={stroke} className="stroke-surface-2" />
        <circle cx={size / 2} cy={size / 2} r={r} fill="none" strokeWidth={stroke} strokeLinecap="round"
          pathLength={100} strokeDasharray={value + " 100"} className="stroke-primary transition-[stroke-dasharray] duration-500" />
      </svg>
      <span className="absolute text-[15px] font-semibold tabular-nums tracking-tight">{value}%</span>
    </span>
  );
}

export function Spinner({ size = 20, label = "Loading" }: { size?: number; label?: string }) {
  return (
    <svg role="status" aria-label={label} width={size} height={size} viewBox="0 0 24 24" className="animate-spin">
      <circle cx="12" cy="12" r="10" fill="none" strokeWidth="3" className="stroke-surface-2" />
      <circle cx="12" cy="12" r="10" fill="none" strokeWidth="3" strokeLinecap="round" pathLength={100} strokeDasharray="28 100" className="stroke-primary" />
    </svg>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.