Progress
A process indicator — determinate or indeterminate, five colours, three sizes, in React (@bpdm/ui) and Angular (@bpdm/ng).
The Progress bar shows how far along a task is — determinate (drive
value, the fill animates to its width) or indeterminate (an animated sweep
when there's no known total). Five colours, three sizes, an optional value/label
row, and accessible by default. In Angular it's <bpdm-progress-bar>.
Usage
import { ProgressBar } from '@bpdm/ui/progress';
export function ProgressUsage() {
return <ProgressBar value={60} />;
}import { Component } from '@angular/core';
import { BpdmProgressBar } from '@bpdm/ng';
@Component({
selector: 'progress-usage',
imports: [BpdmProgressBar],
templateUrl: './progress-usage.html',
})
export class ProgressUsage {}<bpdm-progress-bar [value]="60" />Value & label
Set showValue to show the percentage, and label for leading text (e.g.
"Uploading…"). They sit in a row above the bar.
import { ProgressBar } from '@bpdm/ui/progress';
export function UploadProgress() {
return <ProgressBar value={72} showValue label="Uploading…" variant="success" />;
}import { Component } from '@angular/core';
import { BpdmProgressBar } from '@bpdm/ng';
@Component({
selector: 'upload-progress',
imports: [BpdmProgressBar],
templateUrl: './progress-label.html',
})
export class UploadProgress {}<bpdm-progress-bar [value]="72" showValue label="Uploading…" variant="success" />Value inside the bar
Set valuePosition="inside" to render the value within the bar itself — handy
in tight layouts.
import { ProgressBar } from '@bpdm/ui/progress';
export function InsideProgress() {
return <ProgressBar value={72} valuePosition="inside" />;
}import { Component } from '@angular/core';
import { BpdmProgressBar } from '@bpdm/ng';
@Component({
selector: 'inside-progress',
imports: [BpdmProgressBar],
templateUrl: './progress-inside.html',
})
export class InsideProgress {}<bpdm-progress-bar [value]="72" valuePosition="inside" />Custom format
Pass format to replace the percentage with your own label — e.g. a value/max
readout. It implies showValue.
import { ProgressBar } from '@bpdm/ui/progress';
export function StorageBar() {
return (
<ProgressBar value={50} max={100} label="Storage" format={(v, max) => `${v}/${max} GB`} />
);
}format is a function input — define it on the component and bind it.
import { Component } from '@angular/core';
import { BpdmProgressBar } from '@bpdm/ng';
@Component({
selector: 'storage-bar',
imports: [BpdmProgressBar],
templateUrl: './progress-format.html',
})
export class StorageBar {
readonly storageFormat = (v: number, max: number) => `${v}/${max} GB`;
}<bpdm-progress-bar [value]="50" [max]="100" label="Storage" [format]="storageFormat" />Sizes
The size prop offers sm, md (default), and lg.
import { ProgressBar } from '@bpdm/ui/progress';
export function ProgressSizes() {
return (
<div className="flex flex-col gap-4">
<ProgressBar size="sm" value={62} />
<ProgressBar size="md" value={62} />
<ProgressBar size="lg" value={62} />
</div>
);
}import { Component } from '@angular/core';
import { BpdmProgressBar } from '@bpdm/ng';
@Component({
selector: 'progress-sizes',
imports: [BpdmProgressBar],
templateUrl: './progress-sizes.html',
})
export class ProgressSizes {}<bpdm-progress-bar size="sm" [value]="62" />
<bpdm-progress-bar size="md" [value]="62" />
<bpdm-progress-bar size="lg" [value]="62" />Colours
The variant prop offers primary (default), success, warning,
destructive, and info.
import { ProgressBar } from '@bpdm/ui/progress';
export function ProgressColours() {
return (
<div className="flex flex-col gap-3">
<ProgressBar variant="primary" value={18} showValue label="primary" />
<ProgressBar variant="success" value={36} showValue label="success" />
<ProgressBar variant="warning" value={54} showValue label="warning" />
<ProgressBar variant="destructive" value={72} showValue label="destructive" />
<ProgressBar variant="info" value={90} showValue label="info" />
</div>
);
}import { Component } from '@angular/core';
import { BpdmProgressBar } from '@bpdm/ng';
@Component({
selector: 'progress-colours',
imports: [BpdmProgressBar],
templateUrl: './progress-colours.html',
})
export class ProgressColours {}<bpdm-progress-bar variant="primary" [value]="18" showValue label="primary" />
<bpdm-progress-bar variant="success" [value]="36" showValue label="success" />
<bpdm-progress-bar variant="warning" [value]="54" showValue label="warning" />
<bpdm-progress-bar variant="destructive" [value]="72" showValue label="destructive" />
<bpdm-progress-bar variant="info" [value]="90" showValue label="info" />Indeterminate
When there's no known total, set indeterminate for an animated sweep.
import { ProgressBar } from '@bpdm/ui/progress';
export function IndeterminateProgress() {
return <ProgressBar indeterminate />;
}import { Component } from '@angular/core';
import { BpdmProgressBar } from '@bpdm/ng';
@Component({
selector: 'indeterminate-progress',
imports: [BpdmProgressBar],
templateUrl: './progress-indeterminate.html',
})
export class IndeterminateProgress {}<bpdm-progress-bar indeterminate />Dynamic value
Drive value from state and the fill animates smoothly to each new width. Switch
the variant to success when it completes.
import { useEffect, useState } from 'react';
import { ProgressBar } from '@bpdm/ui/progress';
export function Syncing() {
const [value, setValue] = useState(20);
useEffect(() => {
const id = setInterval(() => {
setValue((v) => (v >= 100 ? 0 : Math.min(100, v + 10)));
}, 700);
return () => clearInterval(id);
}, []);
return (
<ProgressBar value={value} showValue label="Syncing" variant={value >= 100 ? 'success' : 'primary'} />
);
}import { Component, signal } from '@angular/core';
import { BpdmProgressBar } from '@bpdm/ng';
@Component({
selector: 'syncing',
imports: [BpdmProgressBar],
templateUrl: './progress-dynamic.html',
})
export class Syncing {
readonly value = signal(20);
constructor() {
setInterval(() => this.value.update((v) => (v >= 100 ? 0 : Math.min(100, v + 10))), 700);
}
}<bpdm-progress-bar [value]="value()" showValue label="Syncing"
[variant]="value() >= 100 ? 'success' : 'primary'" />In a card
A real-world layout — a progress bar inside a file-upload card, climbing on
Upload and flipping to success at 100%.
report-2025.pdf
4.2 MB
import { useState } from 'react';
import { ProgressBar } from '@bpdm/ui/progress';
import { Button } from '@bpdm/ui/button';
export function UploadCard() {
const [value, setValue] = useState(0);
const [uploading, setUploading] = useState(false);
const start = () => {
setUploading(true);
setValue(0);
const id = setInterval(() => {
setValue((v) => {
if (v >= 100) {
clearInterval(id);
setUploading(false);
return 100;
}
return v + 8;
});
}, 220);
};
return (
<div className="w-72 space-y-4 rounded-xl border bg-card p-5 shadow-sm">
<div>
<p className="text-sm font-medium">report-2025.pdf</p>
<p className="text-xs text-muted-foreground">4.2 MB</p>
</div>
<ProgressBar value={value} showValue variant={value >= 100 ? 'success' : 'primary'} />
<Button size="sm" variant="secondary" appearance="outline" onClick={start} disabled={uploading}>
{value >= 100 ? 'Upload again' : 'Upload'}
</Button>
</div>
);
}import { Component, signal } from '@angular/core';
import { BpdmProgressBar, BpdmButton } from '@bpdm/ng';
@Component({
selector: 'upload-card',
imports: [BpdmProgressBar, BpdmButton],
templateUrl: './progress-in-card.html',
})
export class UploadCard {
readonly value = signal(0);
readonly uploading = signal(false);
start() {
this.uploading.set(true);
this.value.set(0);
const id = setInterval(() => {
this.value.update((v) => {
if (v >= 100) {
clearInterval(id);
this.uploading.set(false);
return 100;
}
return v + 8;
});
}, 220);
}
}<div class="w-72 space-y-4 rounded-xl border bg-card p-5 shadow-sm">
<div>
<p class="text-sm font-medium">report-2025.pdf</p>
<p class="text-xs text-muted-foreground">4.2 MB</p>
</div>
<bpdm-progress-bar [value]="value()" showValue [variant]="value() >= 100 ? 'success' : 'primary'" />
<button bpdmButton size="sm" variant="secondary" appearance="outline" (click)="start()" [disabled]="uploading()">
{{ value() >= 100 ? 'Upload again' : 'Upload' }}
</button>
</div>Internationalization
The value and label text is author-supplied — you set the leading label, and
for the value you take the default percentage or supply your own format — so you
localise it in your own app exactly like the rest of the content you pass in. A
determinate bar's aria-valuetext follows your format, so to localise the
announced value, localise format (e.g. with Intl.NumberFormat).
The two strings the component owns are set via a single messages object — a
Partial<ProgressMessages> merged over the English defaults, so override one key or
both:
-
label— the fallback accessible name, used only when you don't pass a stringlabel. Default"Progress". -
loading— thearia-valuetextannounced whileindeterminate(no known value). Default"Loading". -
React — pass
messagesto<ProgressBar messages={{ … }} />. -
Angular — bind
[messages]on<bpdm-progress-bar>.
The bar mirrors under dir="rtl" with no extra prop — the fill grows from the
inline-start, the indeterminate sweep and the inside-value follow the writing
direction, and the label/value header flips automatically.
The example below localises the built-in strings to German via messages (the
fallback name and the loading text) and localises the announced value through
format with Intl.NumberFormat — any visible copy you pass stays in your app's
own content:
import { ProgressBar, type ProgressMessages } from '@bpdm/ui/progress';
// Only the built-in strings are translated.
const messages: Partial<ProgressMessages> = {
label: 'Fortschritt',
loading: 'Wird geladen',
};
// Localise the announced value by localising `format`.
const percent = new Intl.NumberFormat('de-DE', { style: 'percent' });
export function UploadProgress() {
return (
<ProgressBar
value={72}
showValue
messages={messages}
format={(v, max) => percent.format(v / max)}
/>
);
}import { Component } from '@angular/core';
import { BpdmProgressBar, type ProgressMessages } from '@bpdm/ng';
@Component({
selector: 'upload-progress',
imports: [BpdmProgressBar],
templateUrl: './progress-i18n.html',
})
export class UploadProgress {
// Only the built-in strings are translated.
readonly messages: Partial<ProgressMessages> = {
label: 'Fortschritt',
loading: 'Wird geladen',
};
// Localise the announced value by localising `format`.
private readonly percent = new Intl.NumberFormat('de-DE', { style: 'percent' });
readonly percentFormat = (v: number, max: number) => this.percent.format(v / max);
}<bpdm-progress-bar [value]="72" showValue [messages]="messages" [format]="percentFormat" />API
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | 0 | Current value (ignored when indeterminate). Angular: [value]. |
max | number | 100 | Maximum value. Angular: [max]. |
indeterminate | boolean | false | Animated sweep when there's no known total. Angular: [indeterminate]. |
size | sm | md | lg | md | Track height. Angular: [size]. |
variant | primary | success | warning | destructive | info | primary | Fill colour. Angular: [variant]. |
showValue | boolean | false | Show the value (defaults to the percentage). Angular: showValue. |
valuePosition | outside | inside | outside | Value in a row above, or inside the bar. Angular: [valuePosition]. |
format | (value, max) => ReactNode | — | Custom value label. Implies showValue. Angular: a (value, max) => string function input, [format]. |
label | ReactNode (Angular: string) | — | Leading text shown opposite the value; a string label is also the accessible name. Angular: [label]. |
className | string | — | React — extra classes on the root element. |
messages | Partial<ProgressMessages> | English | Override the translatable strings — the fallback accessible name + the indeterminate loading text (see below). Angular: [messages]. |
In Angular, every prop above is an @Input on <bpdm-progress-bar>: format is a
function input (define the function on the component and bind [format]) and
label is a string. In React all other native <div> attributes are forwarded
onto the root element.
App-wide copy — messages
Localise the two built-in strings — the fallback accessible name and the
indeterminate loading text — in one place (see
Internationalization). It's a Partial<ProgressMessages>
merged over the English defaults — override one key or both.
- React — the
messagesprop on<ProgressBar>. - Angular — the
[messages]input on<bpdm-progress-bar>.
ProgressMessages key | Default | Description |
|---|---|---|
label | Progress | Fallback accessible name, used only when no string label is given. |
loading | Loading | aria-valuetext announced while indeterminate. |
Accessibility
- The bar is a
role="progressbar"carryingaria-valuemin,aria-valuemax, andaria-valuenow, plus anaria-valuetextthat reflects the formatted value — so assistive tech announces the same readout your users see. - A determinate bar exposes its exact value; an indeterminate one omits
aria-valuenow/aria-valuemax, setsaria-busy, and announcesmessages.loading(default"Loading"), so a screen reader knows work is ongoing without a number. - Give the bar a name: pass a string
label(e.g. "Uploading…"), or rely on themessages.labelfallback (default"Progress") when you don't — either becomes the bar's accessible name. - Progress is conveyed as text, never by colour alone — the
variantfill and the soft top gloss are decorative (aria-hidden), so the value reads clearly for everyone. - Everything mirrors under
dir="rtl"— the fill grows from the inline-start and the indeterminate sweep + inside-value follow the writing direction; the value stays translatable throughformatand the fallback name / loading text throughmessages.
Badge
A compact label for status, counts and tags — tones, appearances, status dots, removable chips, and corner notifications, in React (@bpdm/ui) and Angular (@bpdm/ng).
Spinner
A loading indicator in six looks — sizes xs–xl, colour-inheriting, with a LoadingOverlay, in React (@bpdm/ui) and Angular (@bpdm/ng).