<progress>

Displays a determinate or indeterminate progress bar from a numeric value and max.

Signature
<progress value={0.5} />

Parameters

ParameterTypeDescription
$valueoptionalnumber | nullA number between 0 and max, or null for indeterminate progress. Specifies how much was done.
$maxoptionalnumber= 1Specifies the maximum value.
$...propsoptionalHTMLProgressElement props<progress> supports all common element props, including children (fallback text for very old browsers), aria-label, className, style and ref.

Return value

ReactElementRenders a browser <progress> element, determinate when value is a number and indeterminate when value is null.

<progress> shows how far along a task is. Two props do all the work: value for the amount done and max for the total. max defaults to 1, so <progress value={0.5} /> is a half-full bar — pass max={100} if you would rather think in percentages.

When to reach for it

Use it for anything with a known scale: an upload's bytes sent, a wizard's completed steps, a quota's usage. Pair it with useFormStatus to show an indeterminate bar while a form action is in flight. Reach for a spinner instead when there is no meaningful denominator and no value to report.

What trips people up

  • Omitting value entirely and passing value={null} look the same on screen — both render the indeterminate animation — so a variable that is accidentally undefined silently becomes a barber pole rather than a zero-length bar.
  • The default max of 1 catches people who pass a percentage: value={40} without max renders as full.
  • Styling it is not ordinary CSS. Browsers expose the fill through vendor pseudo-elements, so many teams render a styled div with role="progressbar" instead.
  • Give it a label. On its own the element announces a number with no idea what it counts.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Show upload completion as bytes transferred against total size
  • Display an indeterminate bar while a form action is pending
  • Indicate step 3 of 5 in a multi-step wizard

Frequently asked questions

Why is my progress bar always full?

Because max defaults to 1 and you are passing a percentage. <progress value={40} /> means 40 out of 1, which clamps to full. Pass max={100} alongside a percentage value, or convert the value to a fraction between 0 and 1.

How do I show an indeterminate progress bar in React?

Pass value={null}. The browser then draws its indeterminate animation instead of a filled portion. Watch out for the inverse: a value that is accidentally undefined produces the same animation, so a bar that should read zero can look like an active loading state.

How do I style the progress element?

Styling requires vendor pseudo-elements — ::-webkit-progress-value, ::-webkit-progress-bar and ::-moz-progress-bar — plus appearance: none, and the rules differ per browser. Many teams instead render a div with role="progressbar" and aria-valuenow/valuemin/valuemax, which styles like any other element while keeping the same accessibility semantics.

More examples

An indeterminate bar driven by useFormStatus

Before submitting, the bar sits empty at value 0 next to the word Idle. Clicking Save flips value to null for 1200ms, which is what makes the browser draw the indeterminate animation, and the counter increments once the action resolves. The bar lives in its own child component because useFormStatus only reports on a form above the component that calls it.

Example · jsx
import { useState } from "react";
import { useFormStatus } from "react-dom";

function SubmitProgress() {
  const { pending } = useFormStatus();
  return (
    <p>
      <progress value={pending ? null : 0} max={1} aria-label="Save progress" />{" "}
      {pending ? "Saving..." : "Idle"}
    </p>
  );
}

export default function SaveForm() {
  const [saved, setSaved] = useState(0);

  async function save() {
    await new Promise((resolve) => setTimeout(resolve, 1200));
    setSaved((count) => count + 1);
  }

  return (
    <form action={save}>
      <input name="title" defaultValue="Draft" />
      <button type="submit">Save</button>
      <SubmitProgress />
      <p>Saves completed: {saved}</p>
    </form>
  );
}

Steps completed, and a styled alternative

Both bars start at two fifths and advance together as Next is pressed, ending at Step 5 of 5: Done. The native <progress> needs no CSS but resists it; the div below reproduces the same state with role="progressbar" and the matching aria-valuenow/min/max attributes, which is the usual escape hatch when a design system needs full control over the fill's colour, height and radius.

Example · jsx
import { useState } from "react";

const STEPS = ["Account", "Address", "Payment", "Review", "Done"];

function StyledBar({ value, max }) {
  const pct = Math.round((value / max) * 100);
  return (
    <div
      role="progressbar"
      aria-valuenow={value}
      aria-valuemin={0}
      aria-valuemax={max}
      style={{ background: "#eee", height: 8, width: 200 }}
    >
      <div style={{ background: "#333", height: "100%", width: pct + "%" }} />
    </div>
  );
}

export default function Wizard() {
  const [step, setStep] = useState(2);
  return (
    <div>
      <p>
        Step {step} of {STEPS.length}: {STEPS[step - 1]}
      </p>
      <progress value={step} max={STEPS.length} aria-label="Signup progress" />
      <StyledBar value={step} max={STEPS.length} />
      <button onClick={() => setStep((s) => Math.min(STEPS.length, s + 1))}>
        Next
      </button>
    </div>
  );
}

Discussion

  • Be the first to comment on this lesson.