ComponentsFeedback

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.

Importing people
x
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.

Importing people
x
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 finished
x
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 finished, 140 skipped
x
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.

Finding people… 214 so far
x
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.

Reading your mailbox
x
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".

Reading your calendar
x
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>
  );
}

On this page