<bpdm/ui />
Data

Stat Card

A dashboard KPI card — label, big value, a good/bad percentage delta and an icon — in React (@bpdm/ui) and Angular (@bpdm/ng).

The Stat Card is a dashboard KPI tile: a label, a big pre-formatted value, an optional percentage delta (coloured green/red by whether the change is good), and an optional icon badge. Tint the whole card with an accent colour. In Angular it's <bpdm-stat-card> with the same inputs; the icon is projected with bpdmStatCardIcon.

Usage

Pass a label and a pre-formatted value. Add a delta (percent change) with an optional deltaLabel, and an icon.

Active users

8,420

vs last week
stat-card-usage.tsx
import { StatCard } from '@bpdm/ui/stat-card';
import { Users } from 'lucide-react'; // any icon works

export function ActiveUsers() {
  return (
    <StatCard label="Active users" value="8,420" delta={3.1} deltaLabel="vs last week" icon={<Users />} />
  );
}

The icon is any element marked bpdmStatCardIcon (projected into the badge).

stat-card-usage.ts
import { Component } from '@angular/core';
import { BpdmStatCard } from '@bpdm/ng';

@Component({
  selector: 'active-users',
  imports: [BpdmStatCard],
  template: `
    <bpdm-stat-card label="Active users" value="8,420" [delta]="3.1" deltaLabel="vs last week">
      <svg bpdmStatCardIcon viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
        <circle cx="9" cy="7" r="4" />
      </svg>
    </bpdm-stat-card>
  `,
})
export class ActiveUsers {}

Delta & direction

A positive delta is green and a negative one red — by default up is good. For metrics where an increase is bad (churn, bounce rate, error rate…), set positiveIsGood={false} so a rise turns red. A zero delta is neutral. The arrow and sign convey direction too, so it never relies on colour alone.

New signups

1,294

vs last month

Avg. session

4m 12s

vs last month

Bounce rate

2.4%

vs last month
stat-card-delta.tsx
import { StatCard } from '@bpdm/ui/stat-card';
import { UserPlus, Clock, TrendingDown } from 'lucide-react';

export function Deltas() {
  return (
    <div className="grid gap-4 sm:grid-cols-3">
      {/* up + good → green */}
      <StatCard label="New signups" value="1,294" delta={12.5} deltaLabel="vs last month" icon={<UserPlus />} />
      {/* down + good → red */}
      <StatCard label="Avg. session" value="4m 12s" delta={-1.8} deltaLabel="vs last month" icon={<Clock />} />
      {/* up but bad → red */}
      <StatCard label="Bounce rate" value="2.4%" delta={0.6} positiveIsGood={false} deltaLabel="vs last month" icon={<TrendingDown />} />
    </div>
  );
}
stat-card-delta.ts
import { Component } from '@angular/core';
import { BpdmStatCard } from '@bpdm/ng';

@Component({
  selector: 'deltas',
  imports: [BpdmStatCard],
  template: `
    <div class="grid gap-4 sm:grid-cols-3">
      <bpdm-stat-card label="New signups" value="1,294" [delta]="12.5" deltaLabel="vs last month" />
      <bpdm-stat-card label="Avg. session" value="4m 12s" [delta]="-1.8" deltaLabel="vs last month" />
      <!-- up is bad here → a rise turns red -->
      <bpdm-stat-card label="Bounce rate" value="2.4%" [delta]="0.6" [positiveIsGood]="false" deltaLabel="vs last month" />
    </div>
  `,
})
export class Deltas {}

Accent colour

Pass accent — any CSS colour (hex or a token) — to tint the card background and the icon badge. Great for colour-coding a dashboard.

Page views

1.24M

Sessions

84.3K

Bounce rate

1.8%

New signups

1,294

stat-card-accent.tsx
import { StatCard } from '@bpdm/ui/stat-card';
import { Eye, Activity } from 'lucide-react';

export function Accented() {
  return (
    <div className="grid gap-4 sm:grid-cols-2">
      <StatCard label="Page views" value="1.24M" accent="#2563eb" icon={<Eye />} />
      <StatCard label="Sessions" value="84.3K" accent="#0d9488" icon={<Activity />} />
    </div>
  );
}
stat-card-accent.ts
import { Component } from '@angular/core';
import { BpdmStatCard } from '@bpdm/ng';

@Component({
  selector: 'accented',
  imports: [BpdmStatCard],
  template: `
    <div class="grid gap-4 sm:grid-cols-2">
      <bpdm-stat-card label="Page views" value="1.24M" accent="#2563eb" />
      <bpdm-stat-card label="Sessions" value="84.3K" accent="#0d9488" />
    </div>
  `,
})
export class Accented {}

Without a delta

Omit delta for a plain label + value tile.

Open tickets

37

stat-card-plain.tsx
import { StatCard } from '@bpdm/ui/stat-card';

export function OpenTickets() {
  return <StatCard label="Open tickets" value="37" />;
}
stat-card-plain.ts
import { Component } from '@angular/core';
import { BpdmStatCard } from '@bpdm/ng';

@Component({
  selector: 'open-tickets',
  imports: [BpdmStatCard],
  template: `<bpdm-stat-card label="Open tickets" value="37" />`,
})
export class OpenTickets {}

Loading

Set loading while the data is in flight — the card shows a shimmering skeleton (and sets aria-busy) in place of the content, keeping the layout stable.

stat-card-loading.tsx
import { StatCard } from '@bpdm/ui/stat-card';

export function LiveUsers({ loading, count }: { loading: boolean; count?: number }) {
  return <StatCard label="Active users" value={count?.toLocaleString() ?? '—'} loading={loading} />;
}
stat-card-loading.ts
import { Component, input } from '@angular/core';
import { BpdmStatCard } from '@bpdm/ng';

@Component({
  selector: 'live-users',
  imports: [BpdmStatCard],
  template: `<bpdm-stat-card label="Active users" [value]="value()" [loading]="loading()" />`,
})
export class LiveUsers {
  readonly loading = input(false);
  readonly value = input('—');
}

Dashboard grid

Stat Cards are just tiles — drop them in a responsive grid for a KPI row.

Active users

8,420

vs last week

New signups

1,294

vs last month

Bounce rate

2.4%

vs last month

Avg. session

4m 12s

vs last month
dashboard.tsx
import { StatCard } from '@bpdm/ui/stat-card';
import { Users, UserPlus, TrendingDown, Clock } from 'lucide-react';

export function Dashboard() {
  return (
    <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
      <StatCard label="Active users" value="8,420" delta={3.1} deltaLabel="vs last week" icon={<Users />} />
      <StatCard label="New signups" value="1,294" delta={12.5} deltaLabel="vs last month" icon={<UserPlus />} />
      <StatCard label="Bounce rate" value="2.4%" delta={0.6} positiveIsGood={false} deltaLabel="vs last month" icon={<TrendingDown />} />
      <StatCard label="Avg. session" value="4m 12s" delta={-1.8} deltaLabel="vs last month" icon={<Clock />} />
    </div>
  );
}
dashboard.ts
import { Component } from '@angular/core';
import { BpdmStatCard } from '@bpdm/ng';

@Component({
  selector: 'dashboard',
  imports: [BpdmStatCard],
  template: `
    <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
      <bpdm-stat-card label="Active users" value="8,420" [delta]="3.1" deltaLabel="vs last week" />
      <bpdm-stat-card label="New signups" value="1,294" [delta]="12.5" deltaLabel="vs last month" />
      <bpdm-stat-card label="Bounce rate" value="2.4%" [delta]="0.6" [positiveIsGood]="false" deltaLabel="vs last month" />
      <bpdm-stat-card label="Avg. session" value="4m 12s" [delta]="-1.8" deltaLabel="vs last month" />
    </div>
  `,
})
export class Dashboard {}

API

StatCard / <bpdm-stat-card>

PropTypeDefaultDescription
labelstringRequired. The metric name.
valueReactNode (Angular string)Required. The pre-formatted value, e.g. "8,420".
deltanumberPercent change, e.g. 12.5 or -1.8. Omit for no delta.
deltaLabelstringCaption next to the delta, e.g. "vs last month".
positiveIsGoodbooleantrueWhen false, an increase is shown as bad (red) — for churn/bounce/errors.
iconReactNode (Angular bpdmStatCardIcon)Icon shown in the badge.
accentstringAny CSS colour — tints the card + icon badge.
loadingbooleanfalseShow a shimmering skeleton (sets aria-busy) while data loads.
localestring (BCP 47)runtime defaultFormats the delta number (decimal separator).
messagesPartial<StatCardMessages>EnglishOverride the screen-reader / loading text for i18n (see below).
classNamestring (Angular class)Extra classes on the card.

StatCardMessages: { increased, decreased, noChange, loading }.

Accessibility

  • The card is a role="group" labelled by its label (aria-labelledby), so a screen reader announces it as one named unit, then reads the value and delta.
  • The delta never relies on colour or the arrow alone: the arrow is decorative (aria-hidden) and the delta carries a text alternative — "Increased 12.5%" / "Decreased 3.2%" / "No change" — so direction is announced, not just shown in red/green. deltaLabel follows for context ("vs last month"). The direction words are translatable via messages.
  • The coloured delta text uses the darkened -strong colour tokens, so red (negative) and green (positive) deltas measure at ≥4.5:1 against the card on the light themes (paper, mist) and stay unchanged on the dark themes (charcoal, slate). The arrow glyph is decorative and inherits the same colour.
  • The icon badge is decorative (aria-hidden) — the label carries the meaning.
  • While loading, the card sets aria-busy + aria-live="polite" and is labelled <label> <loading> (the metric name plus a "loading" suffix) so assistive tech knows the value is pending.
  • accent only tints backgrounds; text keeps accessible contrast against the card.

Internationalization

Two knobs make the card locale-ready:

  • locale — a BCP 47 tag that formats the delta number's decimal separator via Intl.NumberFormat (e.g. 12,5% in de-DE).
  • messages — override the screen-reader / loading strings (increased, decreased, noChange, loading); English defaults ship built-in, override any subset.

The visible label, value and deltaLabel are always your own content, so they're translated by you at the call site. Layout is direction-agnostic (flexbox + logical gap), so the card mirrors correctly under dir="rtl" with no configuration.

Aktive Nutzer

8.420

ggü. letzter Woche
stat-card-i18n.tsx
import { StatCard } from '@bpdm/ui/stat-card';

const de = { increased: 'Gestiegen', decreased: 'Gesunken', noChange: 'Keine Änderung', loading: 'wird geladen' };

export function AktiveNutzer() {
  return (
    <StatCard
      label="Aktive Nutzer"
      value="8.420"
      delta={3.1}
      deltaLabel="ggü. letzter Woche"
      locale="de-DE"
      messages={de}
    />
  );
}
stat-card-i18n.ts
import { Component } from '@angular/core';
import { BpdmStatCard, type StatCardMessages } from '@bpdm/ng';

@Component({
  selector: 'aktive-nutzer',
  imports: [BpdmStatCard],
  template: `
    <bpdm-stat-card
      label="Aktive Nutzer"
      value="8.420"
      [delta]="3.1"
      deltaLabel="ggü. letzter Woche"
      locale="de-DE"
      [messages]="de"
    />
  `,
})
export class AktiveNutzer {
  de: Partial<StatCardMessages> = {
    increased: 'Gestiegen',
    decreased: 'Gesunken',
    noChange: 'Keine Änderung',
    loading: 'wird geladen',
  };
}

On this page