Progress
Linear bar, ring and segmented meter, all spring-driven.
$
pnpm dlx shadcn@latest add https://lodeui.com/r/progress.jsonUploading assets42%
42%
Password strength
components/examples/progress-demo.tsx
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Meter, Progress, ProgressRing } from "@/components/ui/progress";
export default function ProgressDemo() {
const [v, setV] = React.useState(42);
return (
<div className="w-full max-w-sm space-y-6 rounded-xl border border-border bg-card p-6">
<div>
<div className="mb-2 flex justify-between text-sm"><span>Uploading assets</span><span className="tabular-nums text-muted-foreground">{v}%</span></div>
<Progress value={v} warnAt={0.9} aria-label="Upload" />
</div>
<div className="flex items-center gap-4">
<ProgressRing value={v} aria-label="Storage">{v}%</ProgressRing>
<div className="flex-1">
<p className="mb-2 text-sm">Password strength</p>
<Meter value={Math.ceil(v / 20)} aria-label="Password strength" />
</div>
</div>
<div className="flex gap-2">
<Button size="sm" variant="secondary" onClick={() => setV((x) => Math.max(0, x - 15))}>−15</Button>
<Button size="sm" variant="secondary" onClick={() => setV((x) => Math.min(100, x + 15))}>+15</Button>
</div>
</div>
);
}When to use
- Showing how much of a task, quota or upload is done.
When not to use
- Unknown duration: use a spinner or skeleton.
Installation
$
pnpm dlx shadcn@latest add https://lodeui.com/r/progress.json1Install the dependencies
npm install motion2Copy the source into your project
components/ui/progress.tsx"use client"; import * as React from "react"; import { motion } from "motion/react"; import { cn } from "@/lib/utils"; const spring = { type: "spring", stiffness: 120, damping: 22 } as const; export interface ProgressProps { value: number; max?: number; /** Turns the bar to the destructive color at or above this fraction (0–1). */ warnAt?: number; className?: string; "aria-label"?: string; } export function Progress({ value, max = 100, warnAt, className, ...aria }: ProgressProps) { const pct = Math.min(1, Math.max(0, value / max)); const warn = warnAt !== undefined && pct >= warnAt; return ( <div role="progressbar" aria-label={aria["aria-label"]} aria-valuemin={0} aria-valuemax={max} aria-valuenow={value} className={cn("h-1.5 w-full overflow-hidden rounded-full bg-muted", className)} > <motion.div className={cn("h-full rounded-full", warn ? "bg-destructive" : "bg-primary")} initial={{ width: 0 }} animate={{ width: `${pct * 100}%` }} transition={spring} /> </div> ); } export interface ProgressRingProps { value: number; max?: number; size?: number; thickness?: number; children?: React.ReactNode; className?: string; "aria-label"?: string; } export function ProgressRing({ value, max = 100, size = 56, thickness = 5, children, className, ...aria }: ProgressRingProps) { const pct = Math.min(1, Math.max(0, value / max)); const r = (size - thickness) / 2; return ( <div role="progressbar" aria-label={aria["aria-label"]} aria-valuemin={0} aria-valuemax={max} aria-valuenow={value} className={cn("relative inline-grid place-items-center", className)} style={{ width: size, height: size }} > <svg width={size} height={size} className="-rotate-90"> <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="var(--muted)" strokeWidth={thickness} /> <motion.circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="var(--primary)" strokeWidth={thickness} strokeLinecap="round" initial={{ pathLength: 0 }} animate={{ pathLength: pct }} transition={spring} /> </svg> {children && <span className="absolute inset-0 grid place-items-center text-xs font-medium tabular-nums">{children}</span>} </div> ); } export interface MeterProps { /** Segments filled out of `segments`. */ value: number; segments?: number; className?: string; "aria-label"?: string; } /** A segmented meter, e.g. for password strength or a plan's quota. */ export function Meter({ value, segments = 5, className, ...aria }: MeterProps) { return ( <div role="meter" aria-label={aria["aria-label"]} aria-valuemin={0} aria-valuemax={segments} aria-valuenow={value} className={cn("flex gap-1", className)}> {Array.from({ length: segments }, (_, i) => ( <span key={i} className="relative h-1.5 flex-1 overflow-hidden rounded-full bg-muted"> <motion.span className="absolute inset-0 origin-left rounded-full bg-primary" initial={false} animate={{ scaleX: i < value ? 1 : 0 }} transition={{ ...spring, delay: i * 0.04 }} /> </span> ))} </div> ); }
Usage
Import it from @/components/ui/progress after installing.
example.tsx
import { Meter, Progress, ProgressRing } from "@/components/ui/progress";
export function Quota() {
return (
<div className="space-y-3">
<Progress value={812} max={1000} warnAt={0.8} aria-label="API calls" />
<ProgressRing value={64} aria-label="Storage">64%</ProgressRing>
<Meter value={3} segments={5} aria-label="Password strength" />
</div>
);
}API reference
Progress
A linear bar.
| Prop | Type | Default | Description |
|---|---|---|---|
| value / max | number | — / 100 | Amount done. |
| warnAt | number | – | Fraction (0–1) where the bar turns destructive. |
ProgressRing
A circular indicator with optional center content.
| Prop | Type | Default | Description |
|---|---|---|---|
| value / max | number | – | Amount done. |
| size / thickness | number | 56 / 5 | Dimensions in px. |
Meter
Segmented meter for discrete levels like password strength.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | – | Filled segments. |
| segments | number | 5 | Total segments. |
Accessibility
- role="progressbar" or role="meter" with min, max and now.
- Pass aria-label so the measure is named.
Motion
- Springs to each new value; meter segments fill in sequence.
Responsive behavior
- Linear bars fill their container; rings are fixed size.
Performance
- Transform and width animations only.
Notes for AI
- Use warnAt for quotas so 'near limit' is visible.
This page as Markdown, for assistants and coding agents:
/components/progress/markdownThe full library index for assistants is at /llms.txt.
Lode222 doc pagesllms.txt