Skip to main content

Progress

The Progress component visualizes the completion status of a task or process. It supports both determinate progress (with a known value) and indeterminate progress (ongoing process without a known completion time).

Import

import { Progress } from '@base-ui/react/progress';

Anatomy

The Progress component consists of five parts:
  • <Progress.Root> - The container that provides context
  • <Progress.Track> - The background track
  • <Progress.Indicator> - The filled portion showing progress
  • <Progress.Label> - A label for the progress bar
  • <Progress.Value> - Displays the formatted value
<Progress.Root value={60}>
  <Progress.Label>Upload Progress</Progress.Label>
  <Progress.Track>
    <Progress.Indicator />
  </Progress.Track>
  <Progress.Value />
</Progress.Root>

Basic Usage

function FileUpload() {
  const [progress, setProgress] = React.useState(0);

  return (
    <Progress.Root value={progress}>
      <Progress.Label>Uploading file...</Progress.Label>
      <Progress.Track>
        <Progress.Indicator />
      </Progress.Track>
      <Progress.Value />
    </Progress.Root>
  );
}

Key Features

  • Determinate and indeterminate states: Show specific progress or ongoing activity
  • Accessible: Uses ARIA progressbar role
  • Formatted values: Automatic number formatting with Intl.NumberFormat
  • Status tracking: Automatically tracks indeterminate, progressing, and complete states
  • Locale support: Format numbers according to user locale
  • Unstyled: Complete styling control

Component Props

Root

The root container that manages the progress state and provides accessibility attributes. Props:
  • value (number | null, required): The current progress value. Use null for indeterminate state
  • min (number): The minimum value (default: 0)
  • max (number): The maximum value (default: 100)
  • format (Intl.NumberFormatOptions): Options to format the value
  • locale (Intl.LocalesArgument): The locale for number formatting
  • getAriaValueText (function): Custom function to generate aria-valuetext
  • Renders a <div> element with role=“progressbar”
State:
  • status: One of ‘indeterminate’, ‘progressing’, or ‘complete’

Track

The background track of the progress bar. Props:
  • Renders a <div> element
  • Accepts all standard HTML div attributes

Indicator

Visualizes the completion status of the task. Props:
  • Renders a <div> element
  • Width is automatically set based on the value
  • Accepts all standard HTML div attributes

Label

A label for the progress bar. Props:
  • Renders a <label> element
  • Automatically associates with the progress bar for accessibility

Value

Displays the formatted current value. Props:
  • Renders a <span> element
  • Automatically displays the formatted value

Styling

<Progress.Root value={65} className="progress-root">
  <Progress.Label className="progress-label">Loading...</Progress.Label>
  <Progress.Track className="progress-track">
    <Progress.Indicator className="progress-indicator" />
  </Progress.Track>
  <Progress.Value className="progress-value" />
</Progress.Root>
.progress-root {
  width: 300px;
}

.progress-label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 600;
  font-size: 14px;
}

.progress-track {
  position: relative;
  width: 100%;
  height: 8px;
  background-color: #e5e7eb;
  border-radius: 9999px;
  overflow: hidden;
}

.progress-indicator {
  background-color: #3b82f6;
  transition: width 0.3s ease;
  border-radius: 9999px;
}

/* Indeterminate state animation */
.progress-root[data-status="indeterminate"] .progress-indicator {
  animation: progress-indeterminate 1.5s ease-in-out infinite;
}

@keyframes progress-indeterminate {
  0% {
    transform: translateX(-100%);
    width: 30%;
  }
  50% {
    width: 50%;
  }
  100% {
    transform: translateX(400%);
    width: 30%;
  }
}

.progress-value {
  display: block;
  margin-top: 0.5rem;
  font-size: 14px;
  color: #6b7280;
}

Common Patterns

Indeterminate Progress

Use null as the value for indeterminate progress:
<Progress.Root value={null}>
  <Progress.Label>Processing...</Progress.Label>
  <Progress.Track>
    <Progress.Indicator />
  </Progress.Track>
</Progress.Root>

Status-Based Styling

Style based on the progress status:
<Progress.Root 
  value={100}
  render={(state) => (
    <div data-status={state.status}>
      <Progress.Label>
        {state.status === 'complete' ? 'Complete!' : 'Loading...'}
      </Progress.Label>
      <Progress.Track>
        <Progress.Indicator 
          style={{
            backgroundColor: state.status === 'complete' ? '#22c55e' : '#3b82f6'
          }}
        />
      </Progress.Track>
    </div>
  )}
/>

Animated Progress

function AnimatedProgress() {
  const [value, setValue] = React.useState(0);

  React.useEffect(() => {
    const timer = setInterval(() => {
      setValue((prev) => {
        if (prev >= 100) {
          clearInterval(timer);
          return 100;
        }
        return prev + 10;
      });
    }, 500);

    return () => clearInterval(timer);
  }, []);

  return (
    <Progress.Root value={value}>
      <Progress.Label>Installing...</Progress.Label>
      <Progress.Track>
        <Progress.Indicator />
      </Progress.Track>
      <Progress.Value />
    </Progress.Root>
  );
}

Percentage Display

<Progress.Root value={75}>
  <div style={{ display: 'flex', justifyContent: 'space-between' }}>
    <Progress.Label>Download</Progress.Label>
    <Progress.Value />
  </div>
  <Progress.Track>
    <Progress.Indicator />
  </Progress.Track>
</Progress.Root>

Circular Progress

function CircularProgress({ value }) {
  const circumference = 2 * Math.PI * 45;
  const offset = circumference - (value / 100) * circumference;

  return (
    <Progress.Root value={value}>
      <svg width="120" height="120" style={{ transform: 'rotate(-90deg)' }}>
        <circle
          cx="60"
          cy="60"
          r="45"
          fill="none"
          stroke="#e5e7eb"
          strokeWidth="10"
        />
        <circle
          cx="60"
          cy="60"
          r="45"
          fill="none"
          stroke="#3b82f6"
          strokeWidth="10"
          strokeDasharray={circumference}
          strokeDashoffset={offset}
          strokeLinecap="round"
          style={{ transition: 'stroke-dashoffset 0.3s ease' }}
        />
      </svg>
      <Progress.Value 
        style={{
          position: 'absolute',
          top: '50%',
          left: '50%',
          transform: 'translate(-50%, -50%)',
        }}
      />
    </Progress.Root>
  );
}

Multi-Step Progress

function MultiStepProgress({ currentStep, totalSteps }) {
  const progress = (currentStep / totalSteps) * 100;

  return (
    <Progress.Root value={progress}>
      <Progress.Label>
        Step {currentStep} of {totalSteps}
      </Progress.Label>
      <Progress.Track>
        <Progress.Indicator />
      </Progress.Track>
    </Progress.Root>
  );
}