<bpdm/ui />
Misc

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

progress-usage.tsx
import { ProgressBar } from '@bpdm/ui/progress';

export function ProgressUsage() {
  return <ProgressBar value={60} />;
}
progress-usage.ts
import { Component } from '@angular/core';
import { BpdmProgressBar } from '@bpdm/ng';

@Component({
  selector: 'progress-usage',
  imports: [BpdmProgressBar],
  templateUrl: './progress-usage.html',
})
export class ProgressUsage {}
progress-usage.html
<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.

Uploading…72%
progress-label.tsx
import { ProgressBar } from '@bpdm/ui/progress';

export function UploadProgress() {
  return <ProgressBar value={72} showValue label="Uploading…" variant="success" />;
}
progress-label.ts
import { Component } from '@angular/core';
import { BpdmProgressBar } from '@bpdm/ng';

@Component({
  selector: 'upload-progress',
  imports: [BpdmProgressBar],
  templateUrl: './progress-label.html',
})
export class UploadProgress {}
progress-label.html
<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.

72%72%
progress-inside.tsx
import { ProgressBar } from '@bpdm/ui/progress';

export function InsideProgress() {
  return <ProgressBar value={72} valuePosition="inside" />;
}
progress-inside.ts
import { Component } from '@angular/core';
import { BpdmProgressBar } from '@bpdm/ng';

@Component({
  selector: 'inside-progress',
  imports: [BpdmProgressBar],
  templateUrl: './progress-inside.html',
})
export class InsideProgress {}
progress-inside.html
<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.

Storage50/100 GB
progress-format.tsx
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.

progress-format.ts
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`;
}
progress-format.html
<bpdm-progress-bar [value]="50" [max]="100" label="Storage" [format]="storageFormat" />

Sizes

The size prop offers sm, md (default), and lg.

progress-sizes.tsx
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>
  );
}
progress-sizes.ts
import { Component } from '@angular/core';
import { BpdmProgressBar } from '@bpdm/ng';

@Component({
  selector: 'progress-sizes',
  imports: [BpdmProgressBar],
  templateUrl: './progress-sizes.html',
})
export class ProgressSizes {}
progress-sizes.html
<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.

primary18%
success36%
warning54%
destructive72%
info90%
progress-colours.tsx
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>
  );
}
progress-colours.ts
import { Component } from '@angular/core';
import { BpdmProgressBar } from '@bpdm/ng';

@Component({
  selector: 'progress-colours',
  imports: [BpdmProgressBar],
  templateUrl: './progress-colours.html',
})
export class ProgressColours {}
progress-colours.html
<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.

progress-indeterminate.tsx
import { ProgressBar } from '@bpdm/ui/progress';

export function IndeterminateProgress() {
  return <ProgressBar indeterminate />;
}
progress-indeterminate.ts
import { Component } from '@angular/core';
import { BpdmProgressBar } from '@bpdm/ng';

@Component({
  selector: 'indeterminate-progress',
  imports: [BpdmProgressBar],
  templateUrl: './progress-indeterminate.html',
})
export class IndeterminateProgress {}
progress-indeterminate.html
<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.

Syncing20%
progress-dynamic.tsx
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'} />
  );
}
progress-dynamic.ts
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);
  }
}
progress-dynamic.html
<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

0%
progress-in-card.tsx
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>
  );
}
progress-in-card.ts
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);
  }
}
progress-in-card.html
<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 string label. Default "Progress".

  • loading — the aria-valuetext announced while indeterminate (no known value). Default "Loading".

  • React — pass messages to <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:

progress-i18n.tsx
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)}
    />
  );
}
progress-i18n.ts
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);
}
progress-i18n.html
<bpdm-progress-bar [value]="72" showValue [messages]="messages" [format]="percentFormat" />

API

PropTypeDefaultDescription
valuenumber0Current value (ignored when indeterminate). Angular: [value].
maxnumber100Maximum value. Angular: [max].
indeterminatebooleanfalseAnimated sweep when there's no known total. Angular: [indeterminate].
sizesm | md | lgmdTrack height. Angular: [size].
variantprimary | success | warning | destructive | infoprimaryFill colour. Angular: [variant].
showValuebooleanfalseShow the value (defaults to the percentage). Angular: showValue.
valuePositionoutside | insideoutsideValue in a row above, or inside the bar. Angular: [valuePosition].
format(value, max) => ReactNodeCustom value label. Implies showValue. Angular: a (value, max) => string function input, [format].
labelReactNode (Angular: string)Leading text shown opposite the value; a string label is also the accessible name. Angular: [label].
classNamestringReact — extra classes on the root element.
messagesPartial<ProgressMessages>EnglishOverride 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 messages prop on <ProgressBar>.
  • Angular — the [messages] input on <bpdm-progress-bar>.
ProgressMessages keyDefaultDescription
labelProgressFallback accessible name, used only when no string label is given.
loadingLoadingaria-valuetext announced while indeterminate.

Accessibility

  • The bar is a role="progressbar" carrying aria-valuemin, aria-valuemax, and aria-valuenow, plus an aria-valuetext that 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, sets aria-busy, and announces messages.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 the messages.label fallback (default "Progress") when you don't — either becomes the bar's accessible name.
  • Progress is conveyed as text, never by colour alone — the variant fill 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 through format and the fallback name / loading text through messages.

On this page