Progress
Linear progress bar — determinate (0–100) or indeterminate loop.
import { Progress }from "@/components/ui"
Determinate & indeterminate
Upload68%
Processing…
Full source
The complete component, read from src/components/ui/progress.tsx.
progress.tsx
"use client";
import { cn } from "@/lib/utils";
type ProgressTone = "accent" | "warning" | "error";
const TONE: Record<ProgressTone, string> = {
accent: "bg-accent",
warning: "bg-warning",
error: "bg-error",
};
interface ProgressProps {
value?: number | null;
tone?: ProgressTone;
showValue?: boolean;
label?: string;
className?: string;
}
export function Progress({ value, tone = "accent", showValue, label, className }: ProgressProps) {
const indeterminate = value === null || value === undefined;
const pct = indeterminate ? 0 : Math.max(0, Math.min(100, value));
return (
<div className={cn("w-full", className)}>
{(label || showValue) && (
<div className="mb-1.5 flex items-center justify-between text-xs">
{label && <span className="font-medium text-text">{label}</span>}
{showValue && !indeterminate && <span className="tabular-nums text-text-secondary">{Math.round(pct)}%</span>}
</div>
)}
<div
role="progressbar"
aria-valuenow={indeterminate ? undefined : Math.round(pct)}
aria-valuemin={0}
aria-valuemax={100}
aria-label={label ?? "Progress"}
className="h-2 w-full overflow-hidden rounded-full bg-bg-active"
>
{indeterminate ? (
<div className={cn("h-full w-1/3 rounded-full", TONE[tone], "animate-[progress-indeterminate_1.2s_ease-in-out_infinite]")} />
) : (
<div
className={cn("h-full rounded-full transition-[width] duration-300", TONE[tone])}
style={{ width: `${pct}%` }}
/>
)}
</div>
</div>
);
}Dependencies
To use Progress you also need the following. Copy these into your project alongside it.
Recommended folder structure
project structure
src/components/ui/ └─ progress.tsx src/lib/ └─ utils.ts