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
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).
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
Avg. session
4m 12s
Bounce rate
2.4%
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>
);
}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
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>
);
}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
import { StatCard } from '@bpdm/ui/stat-card';
export function OpenTickets() {
return <StatCard label="Open tickets" value="37" />;
}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.
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} />;
}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
New signups
1,294
Bounce rate
2.4%
Avg. session
4m 12s
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>
);
}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>
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Required. The metric name. |
value | ReactNode (Angular string) | — | Required. The pre-formatted value, e.g. "8,420". |
delta | number | — | Percent change, e.g. 12.5 or -1.8. Omit for no delta. |
deltaLabel | string | — | Caption next to the delta, e.g. "vs last month". |
positiveIsGood | boolean | true | When false, an increase is shown as bad (red) — for churn/bounce/errors. |
icon | ReactNode (Angular bpdmStatCardIcon) | — | Icon shown in the badge. |
accent | string | — | Any CSS colour — tints the card + icon badge. |
loading | boolean | false | Show a shimmering skeleton (sets aria-busy) while data loads. |
locale | string (BCP 47) | runtime default | Formats the delta number (decimal separator). |
messages | Partial<StatCardMessages> | English | Override the screen-reader / loading text for i18n (see below). |
className | string (Angular class) | — | Extra classes on the card. |
StatCardMessages: { increased, decreased, noChange, loading }.
Accessibility
- The card is a
role="group"labelled by itslabel(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.deltaLabelfollows for context ("vs last month"). The direction words are translatable viamessages. - The coloured delta text uses the darkened
-strongcolour 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) — thelabelcarries the meaning. - While
loading, the card setsaria-busy+aria-live="polite"and is labelled<label> <loading>(the metric name plus a "loading" suffix) so assistive tech knows the value is pending. accentonly 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 viaIntl.NumberFormat(e.g.12,5%inde-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
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}
/>
);
}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',
};
}Data Table
A fully data-driven table — sorting, selection, pagination, filtering, pinning, expansion and more, in React (@bpdm/ui) and Angular (@bpdm/ng).
Order List
Reorder a collection — select items and move them with the control column or drag-and-drop; filterable, in React (@bpdm/ui) and Angular (@bpdm/ng).