ProgressBar

A bar that shows how far a job has come, or that it runs while nobody knows how far (also called a progress indicator). Reach for it for imports, uploads and anything else that counts towards an end.

import { ProgressBar } from "@umriss-ui/core";

value runs from 0 to 1; values outside are clamped. A screen reader hears the percentage, or valueText where a count says it better.

It has no tone: progress is neither good nor bad, and a job at 90 % is not a warning.

Examples

How far a job has come

A value from 0 to 1 and a label naming what progresses; showLabel puts the percentage beside the bar.

import { ProgressBar } from "@umriss-ui/core";

export default function HowFar() {
  return (
    <div style={{ maxWidth: 420 }}>
      <ProgressBar value={0.42} label="Import of the March invoices" showLabel />
    </div>
  );
}

Count instead of a share

Where a count says it better, pass it as valueText: a screen reader hears “5 of 12 parcels” instead of a percentage.

import { ProgressBar, Stack, Text } from "@umriss-ui/core";

export default function ACount() {
  return (
    <Stack gap={2} style={{ maxWidth: 420 }}>
      <Text size="sm">5 of 12 parcels scanned onto FP 214 K</Text>
      <ProgressBar value={5 / 12} label="Loading of tour T-01" valueText="5 of 12 parcels" />
    </Stack>
  );
}

Run without a known end

Leave out value when nobody knows how far it is; the bar runs and shows no figure.

import { ProgressBar, Stack, Text } from "@umriss-ui/core";

export default function WithoutAnEnd() {
  return (
    <Stack gap={2} style={{ maxWidth: 420 }}>
      <Text size="sm">Waiting for the payment provider to confirm …</Text>
      <ProgressBar label="Waiting for the payment provider" />
    </Stack>
  );
}

Follow a job through its steps

One bar per step and one for the whole, derived from the steps; the step waiting on a person runs without a figure.

import { useState } from "react";
import { Badge, Button, Card, CardBody, CardHeader, Divider, ProgressBar, Stack, Text } from "@umriss-ui/core";

interface Step {
  name: string;
  /** Items to work through; 0 for a step that waits on someone. */
  items: number;
  unit: string;
}

const STEPS: Step[] = [
  { name: "Import the bank statements", items: 4, unit: "accounts" },
  { name: "Match payments to invoices", items: 312, unit: "payments" },
  { name: "Controller's sign-off", items: 0, unit: "" },
  { name: "Post to the ledger", items: 9, unit: "cost centres" },
];

/** The share done per step after `ticks` updates from the server. */
function sharesAfter(ticks: number): number[] {
  let left = ticks;
  return STEPS.map((step) => {
    const needed = step.items === 0 ? 2 : 4;
    const taken = Math.min(left, needed);
    left -= taken;
    return taken / needed;
  });
}

export default function ACloseInSteps() {
  const [ticks, setTicks] = useState(6);
  const shares = sharesAfter(ticks);
  const overall = shares.reduce((sum, share) => sum + share, 0) / STEPS.length;
  const running = shares.findIndex((share) => share < 1);

  return (
    <Card style={{ maxWidth: 560 }}>
      <CardHeader
        eyebrow="Month-end close"
        title="February 2026"
        actions={<Badge>{running === -1 ? "Done" : `Step ${running + 1} of ${STEPS.length}`}</Badge>}
      />
      <CardBody>
        <Stack gap={4}>
          <ProgressBar value={overall} label="The whole close" showLabel />
          <Divider />
          {STEPS.map((step, i) => {
            const share = shares[i]!;
            const waiting = step.items === 0 && i === running;
            return (
              <Stack key={step.name} gap={1}>
                <Text size="sm">{step.name}</Text>
                {waiting ? (
                  <ProgressBar label={step.name} />
                ) : (
                  <ProgressBar
                    value={share}
                    label={step.name}
                    showLabel
                    valueText={step.items > 0 ? `${Math.round(share * step.items)} of ${step.items} ${step.unit}` : undefined}
                  />
                )}
              </Stack>
            );
          })}
          <Stack direction="row" gap={2}>
            <Button onClick={() => setTicks((t) => Math.min(t + 1, 14))}>Advance</Button>
            <Button variant="ghost" onClick={() => setTicks(0)}>
              Start again
            </Button>
          </Stack>
        </Stack>
      </CardBody>
    </Card>
  );
}

When to use something else

Keyboard

The keys of Button apply here.

Accessibility

The role progressbar, named by label - "Progress" if you give none - or by your aria-labelledby. With a value it carries the whole percentage as aria-valuenow, and valueText as aria-valuetext; without one it carries no value, the state a screen reader knows as indeterminate.

It is no live region: a screen reader reads the value when it reaches the bar, and a job that finishes announces nothing. Say the end in words, in an Alert or a Toast.

Under forced colours the track takes an edge and the fill the system's selection colour. With reduced motion the sweep of a bar without a value runs three times slower.

API

ProgressBarProps

NameTypeDefaultDescription
valuenumber—How far the task has come, from 0 to 1. Left out, the bar is indeterminate: the task runs, and nobody knows how far it is.

Shown in: Configurator, How far a job has come, Count instead of a share and 3 more

labelstringthe wording's "Progress"What is progressing, e.g. "Import of the invoice records". A progress bar needs a name from the author.

Shown in: How far a job has come, Count instead of a share, Run without a known end and 3 more

valueTextstring—What a screen reader says instead of the percentage, where a count says it better - "3 of 12 files".

Shown in: Count instead of a share, Follow a job through its steps, A deployment (Stepper)

showLabelbooleanfalseThe percentage beside the bar (Geist Mono). Never for an indeterminate bar - it has no figure to show.

Shown in: Configurator, How far a job has come, Follow a job through its steps and 1 more

refRef<HTMLDivElement>—Receives the component's root element. from RefProp

Shown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more

Also takes every attribute of <div>.

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/core

Getting started

Customising

Layout

Typography

Actions

Forms

Choosing

Dates and times

Feedback

Overlays

Navigation

Data display

API index