Progress bar
A thin bar for work that takes longer than a moment and runs while you keep working: reading a mailbox, finding people, importing a file.
Playground
Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page.
import { Progress } from '@/ui/progress';<Progress value={1240} max={3000}> Importing people</Progress>Default
When you know the total. The label sits on the left and the count on the right, in tabular figures. The fill is ink while the work runs.
import { Progress } from '@/ui/progress';
export default function ProgressDefault() {
return (
<Progress value={1240} max={3000} className="max-w-[480px]">
Importing people
</Progress>
);
}Complete
When value reaches max, the fill turns success green. Change the label to the result.
import { Progress } from '@/ui/progress';
export default function ProgressComplete() {
return (
<Progress value={3000} max={3000} className="max-w-[480px]">
Import finished
</Progress>
);
}Success
appearance="success" makes the fill green at any value, for work that finished short of the total, such as an import that skipped rows.
import { Progress } from '@/ui/progress';
export default function ProgressSuccess() {
return (
<Progress value={2860} max={3000} appearance="success" className="max-w-[480px]">
Import finished, 140 skipped
</Progress>
);
}Unknown total
Pass value={null} when the total isn't known yet. A third of the track pulses at its start; the count beside it says what is known so far.
import { Progress } from '@/ui/progress';
export default function ProgressIndeterminate() {
return (
<div className="flex flex-wrap items-center gap-150">
<span aria-live="polite" className="text-body leading-body text-text">
Finding people… 214 so far
</span>
<Progress value={null} aria-label="Finding people" className="w-60" />
</div>
);
}Running
Press Read again to watch the fill move as the value changes.
import { useEffect, useState } from 'react';
import { Button } from '@/ui/button';
import { Progress } from '@/ui/progress';
export default function ProgressLive() {
const [read, setRead] = useState<number | null>(null);
useEffect(() => {
if (read === null || read >= 3120) return;
const t = setTimeout(() => setRead((r) => Math.min(3120, (r ?? 0) + 390)), 400);
return () => clearTimeout(t);
}, [read]);
return (
<div className="flex w-full max-w-[480px] flex-col gap-200">
<Progress value={read ?? 0} max={3120}>
{read !== null && read >= 3120 ? 'Mailbox read' : 'Reading your mailbox'}
</Progress>
<Button className="self-start" onClick={() => setRead(0)}>
Read again
</Button>
</div>
);
}Your own count
Pass ProgressLabel and ProgressValue as children when the count isn't "x of y".
import { Progress, ProgressLabel, ProgressValue } from '@/ui/progress';
export default function ProgressCustomCount() {
return (
<Progress value={412} max={600} className="max-w-[480px]">
<div className="flex items-center gap-150">
<ProgressLabel>Reading your calendar</ProgressLabel>
<ProgressValue>{(_, value) => `${value} events`}</ProgressValue>
</div>
</Progress>
);
}