Bar and ring indicators with determinate and indeterminate states.
pnpm dlx shadcn@latest add @latheui/progressFirst time? Run npx latheui init once to add the @latheui registry. The CLI then writes the source into your project and installs what it needs. Or add it by hand:
Copy the source into your project.
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
export type ProgressProps = Omit<React.ComponentProps<"div">, "children"> & {
value?: number
max?: number
label: string
variant?: "bar" | "ring"
showValue?: boolean
}
export function Progress({
value,
max = 100,
label,
variant = "bar",
showValue = true,
className,
...props
}: ProgressProps) {
const limit = Number.isFinite(max) && max > 0 ? max : 100
const current =
value === undefined
? undefined
: Math.min(limit, Math.max(0, Number.isFinite(value) ? value : 0))
const percent = current === undefined ? 0 : (current / limit) * 100
return (
<div
role="progressbar"
aria-label={label}
aria-valuemin={0}
aria-valuemax={limit}
aria-valuenow={current}
className={cn(
variant === "bar"
? "w-full space-y-2"
: "relative inline-grid size-24 place-items-center",
className
)}
{...props}
>
{variant === "bar" ? (
<>
<div
aria-hidden="true"
className="flex justify-between gap-3 text-xs"
>
<span>{label}</span>
{showValue && (
<span className="text-muted-foreground tabular-nums">
{current === undefined
? "In progress"
: Math.round(percent) + "%"}
</span>
)}
</div>
<div
aria-hidden="true"
className="h-1.5 overflow-hidden rounded-full bg-muted"
>
<div
style={{ width: current === undefined ? "35%" : percent + "%" }}
className={cn(
"h-full rounded-full bg-primary transition-[width] duration-300 motion-reduce:transition-none",
current === undefined &&
"animate-pulse motion-reduce:animate-none"
)}
/>
</div>
</>
) : (
<>
<svg
aria-hidden="true"
viewBox="0 0 100 100"
className={cn(
"absolute inset-0 size-full -rotate-90",
current === undefined && "animate-spin motion-reduce:animate-none"
)}
>
<circle
cx="50"
cy="50"
r="42"
fill="none"
stroke="var(--muted)"
strokeWidth="6"
/>
<circle
cx="50"
cy="50"
r="42"
fill="none"
stroke="var(--primary)"
strokeWidth="6"
strokeLinecap="round"
pathLength="100"
strokeDasharray="100"
strokeDashoffset={current === undefined ? 75 : 100 - percent}
className="transition-[stroke-dashoffset] duration-300 motion-reduce:transition-none"
/>
</svg>
{showValue && (
<span
aria-hidden="true"
className="text-sm font-medium tabular-nums"
>
{current === undefined ? "…" : Math.round(percent) + "%"}
</span>
)}
</>
)}
</div>
)
}| Prop | Type | Default | Description |
|---|---|---|---|
| label* (required) | string | — | Accessible progress name. |
| value | number | — | Current value. Omit for an indeterminate indicator. |
| max | number | 100 | Upper bound; values are clamped. |
| variant | "bar" | "ring" | "bar" | Indicator shape. |
| showValue | boolean | true | Show percentage text. |
Combine the control with your own layout. Semantic colors follow the active theme.
import * as React from "react"
import { Progress } from "@/components/ui/progress"
import { Button } from "@/components/ui/button"
export default function Demo(){const [value,setValue]=React.useState(64);return <div className="w-full max-w-sm space-y-6"><Progress label="Export progress" value={value}/><div className="flex items-center justify-between"><Progress label="Storage used" value={value} variant="ring"/><Button variant="outline" size="sm" onClick={()=>setValue(value>=100?0:Math.min(100,value+12))}>Advance</Button></div></div>}