5.981
Transactions
€ 12.450,00
Net sales
78,2%
Approval rate
0
Refunds
Overview
A metrics group is a horizontal row of small tinted cards, one per metric. Each card holds a single number — count, currency, or percentage — and a single secondary label. The group is the wrapper that lays the cards out; the card is the atom. Use them at the top of a report, a dashboard tab, or a settlement summary so the user gets the headline numbers in one fixation.
One metric per card, one card per metric
The card carries one value and one label — full stop. No deltas inside the card, no trend arrows, no inline mini-charts. When a number needs supporting context (a % delta, a sparkline, a week-over-week comparison), promote it into a dedicated metrics-tile or KPI-card composition. The metrics group stays quiet so the dashboard underneath it can do the heavy lifting.
Anatomy
Three parts inside the card. The wrapping group adds two more — gap and width behaviour — that turn a row of cards into a single scanner-friendly band.
78,2%
Approval rate
Surface
Tinted card on
background.accent.overlay.light.subtler— a 4% black overlay that sits softly on any surrounding surface (white, cream, neutral). 16 px padding inside; 8 px corner radius on desktop, 4 px on mobile.Value
Inter Tight Semibold 20 / 24 (
heading.md), tabular numerals + slashed zero. Pre-format withIntl.NumberFormatper the locale before passing in.Label
Inter Tight Regular 14 / 20, in
text.accent.neutral(#616368, neutral.700). Sentence case. Sits on the next line; 2 px gap between value and label.Info icon (optional)
16 px
infoglyph in the label colour. Sits 4 px after the label, with a tooltip that explains how the metric is computed.
Sizes
Two layouts cover the surfaces in the product. Desktop fills a row at any width with flex-1 cards; mobile wraps to two cards per row at fixed half widths.
Desktop
8 px corner radius. Cards stretch to fill the row, separated by 16 px gaps. Default for product surfaces ≥ 768 px viewport.
5.981
Transactions
€ 12.450,00
Net sales
78,2%
Approval rate
0
Refunds
Mobile
4 px corner radius. Two cards per row, wrapping below 600 px. Cards keep their internal padding; the surrounding gaps stay 16 px.
5.981
Transactions
€ 12.450,00
Net sales
78,2%
Approval
0
Refunds
Content
Numbers come pre-formatted. The card doesn't format anything — it just renders the string passed in, with tabular figures so a row of cards stacks on the decimal point. Use Intl.NumberFormatwith the user's locale upstream.
5.981
Transactions
€ 12.450,00
Net sales
78,2%
Approval rate
0
Refunds
Info icon
The info icon turns the label into a help anchor. Use it sparingly — most labels (Transactions, Refunds, Net sales) speak for themselves. The icon earns its place when the metric's formula or scope is non-obvious — “Approval rate”, “Chargebacks”, “Median ticket”.
Without info
5.981
Transactions
The default. Use when the label name is self-explanatory.
With info
78,2%
Approval rate
Use when the metric's definition isn't obvious from the label. The icon opens a tooltip on hover/focus that explains how the number is computed.
In product
A real surface from the merchant dashboard — the daily end-of-day report. Six metrics, one card each, half-width on mobile, six across on desktop. The header carries the date and a link to the full report; the metrics row is the headline.
End-of-day report
10 February 2026
1.114,00 €
Net sales
92
Transactions
98,4%
Approval rate
12,12 €
Avg. ticket
0
Refunds
0,00 €
Chargebacks
Behavior
- Cards stretch, never crop. On desktop, every card uses
flex: 1 0 0with amin-width: 0so the row distributes evenly. The value text never truncates — if a number won't fit, it wraps. - Cap the row at six on desktop. Below seven, a row reads as one band. At seven or more, the eye loses the line and starts processing them as a list — at which point a real list is the right component.
- Mobile wraps to two columns. Below 600 px, every card sits at
calc(50% - 8px)and wraps. Don't scroll horizontally; the user shouldn't need to swipe to see the headline numbers. - Cards are not interactive. No hover state on the surface; no click handler on the card itself. The info glyph is the one tappable surface inside, and it opens a tooltip — not a route. To navigate to a detail view, put a link in the header above the group.
- Empty values render as “0” or “—”. Never empty string, never null. A zero is a meaningful metric; a dash signals the metric is unavailable for this period.
Accessibility
- The group is the unit. Wrap
MetricsGroupinrole="group"with anaria-labelthat names the report — “Today’s metrics”, “Daily totals”. Screen readers announce the label once and step through each card in DOM order. - Card semantics: each card is a plain
<div>with no ARIA role. The value and label are separate paragraphs so they announce in order — number first, label second — which mirrors visual reading. - Info icons get an explicit label. The icon's
aria-labelreads the explanation, not the word “info”. The tooltip shows the same copy on hover/focus for sighted users. - Tabular numerals are an accessibility feature. They make values comparable in a glance for sighted users. Combine with
slashed-zeroso a zero never reads as a capital O. - Screen-reader value formatting: if the displayed value is
€ 1.234,56, the accessible name should be1234.56 EUR— screen readers read the symbol literally, so format the announcement separately when the screen-reader pronunciation differs from the visual.
Code
The component composes from MetricsGroup + MetricCard. The group is a flex row; the card is the tile. The wrapper MetricCardSlot handles the desktop flex-1 / mobile half-width sizing.
tsx
import { MetricsGroup, MetricCard, MetricCardSlot } from "@flatpay-dk/ui";
// Format upstream — never inside the card
const fmt = (n: number, currency = "EUR") =>
new Intl.NumberFormat("da-DK", {
style: "currency",
currency,
minimumFractionDigits: 2,
}).format(n);
// Daily totals — desktop layout
<MetricsGroup label="Today's metrics">
<MetricCardSlot>
<MetricCard value="92" label="Transactions" />
</MetricCardSlot>
<MetricCardSlot>
<MetricCard value={fmt(1114)} label="Net sales" />
</MetricCardSlot>
<MetricCardSlot>
<MetricCard
value="98,4%"
label="Approval rate"
info
infoDescription="Successful authorisations over the report period."
/>
</MetricCardSlot>
<MetricCardSlot>
<MetricCard value="0" label="Refunds" />
</MetricCardSlot>
</MetricsGroup>
// Mobile layout — two cards per row
<MetricsGroup size="mobile" label="Today's metrics">
{metrics.map((m) => (
<MetricCardSlot key={m.id} size="mobile">
<MetricCard value={fmt(m.value)} label={m.label} size="mobile" />
</MetricCardSlot>
))}
</MetricsGroup>
// Unavailable metric — render an em dash, not an empty string
<MetricCard value="—" label="Median ticket" />Best practices
Metrics groups multiply across the product. A small habit applied consistently keeps every dashboard reading like one publication.
92
Transactions
1.114 €
Net sales
98,4%
Approval
Do
Use the metrics group for the headline numbers at the top of a report. One row, four to six cards, scanning order matters.
5.981
Transactions
Don't
Don't add deltas, sparklines, or trend arrows inside the card. The card is one number; trends belong in a dedicated KPI tile.
98,4%
Approval rate
Do
Add the info icon when the metric's formula isn't obvious from its label. The tooltip earns its place on Approval rate, not on Refunds.
92
Transactions
1.114 €
Net sales
98%
Approval
Don't
Don't add the info icon to every card. A row of six info dots reads as decoration; the user stops reading them after the third.
92
Transactions
1.114 €
Net sales
98%
Approval
0
Refunds
Do
Cap the row at six. Past that, the eye loses the band — you've made a list, not a metrics row.
99
Tx
99
Tx
99
Tx
99
Tx
99
Tx
99
Tx
99
Tx
99
Tx
99
Tx
99
Tx
99
Tx
99
Tx
Don't
Don't pack twelve cards into one row. The cards shrink past readability and the row stops being scannable.
Props
MetricCard
| Prop | Type | Default | Description |
|---|---|---|---|
| value | ReactNode | — | The number to display, pre-formatted as a string. Use Intl.NumberFormat upstream — the card doesn't format anything. |
| label | string | — | Single line label below the value. Inter Tight Regular 14/20 in text.accent.neutral. Sentence case. |
| info | boolean | false | Renders a 16 px info glyph next to the label. Pair with infoDescription for the tooltip + accessible name. |
| infoDescription | string | — | Aria-label and tooltip text for the info glyph. Required when info is true. |
| size | "desktop" | "mobile" | "desktop" | Sets the corner radius — 8 px on desktop, 4 px on mobile. Matches the surrounding MetricsGroup size. |
MetricsGroup
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "desktop" | "mobile" | "desktop" | Layout mode. Desktop fills a row with flex-1 cards; mobile wraps two cards per row at fixed half widths. |
| label | string | — | Group aria-label — names the report or section the metrics belong to. Required. |
| children | ReactNode | — | MetricCardSlot wrappers around MetricCard nodes. Cap at 6 cards on desktop; mobile wraps as needed. |