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.
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>
);
}
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>
);
}
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>
);
}
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>
);
}
The keys of Button apply here.
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.
ProgressBarProps| Name | Type | Default | Description |
|---|---|---|---|
value | number | — | 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 |
label | string | the 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 |
valueText | string | — | 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) |
showLabel | boolean | false | The 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 |
ref | Ref<HTMLDivElement> | — | Receives the component's root element. from RefPropShown 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>.
What umriss deliberately does not build, and why: ADR-0032.