Overview
The Stat component (also called Metric, KPI, or Statistic) presents a single numerical value prominently with a supporting label and optional trend indicator. It is the workhorse of dashboards, summary headers, and analytics pages — anywhere a user needs to quickly absorb a data point without parsing a chart.
A well-designed Stat component does three things simultaneously: it tells you what is being measured (the label), how much (the value), and which direction things are moving (the trend). When all three register within a single fixation, the component has done its job.
When to use a Stat:
- Dashboard overview cards displaying KPIs (revenue, users, conversion rate)
- Summary rows at the top of reports or admin panels
- Pricing pages showing feature limits or plan comparisons
- Profile pages displaying followers, posts, or engagement counts
When NOT to use a Stat:
- For data that needs temporal context — use a chart with axis labels instead
- For comparisons across multiple dimensions — use a Table or chart
- For status information (online/offline, healthy/unhealthy) — use a Badge
- For progress toward a goal — use a Progress Bar
Large numeric values should be formatted with locale-aware separators (e.g., 1,234,567 in en-US, 1.234.567 in de-DE). Use abbreviations for extremely large numbers (1.2M, 3.4B) but provide the full value via title attribute or Tooltip. Test your stat label contrast with the Contrast Checker to ensure readability across themes.
Variants
Common Stat Variants
| Variant | Purpose | Visual Treatment |
|---|---|---|
| Default | Standard KPI display. | Label above or below a large value. Minimal decoration. |
| With Trend | Shows direction of change (up/down). | Includes an arrow icon and percentage delta, colored green (positive) or red (negative). |
| With Icon | Provides category context at a glance. | Leading icon (e.g., dollar sign, users icon) in a colored circle or square. |
| With Sparkline | Adds micro-trend context without a full chart. | Small inline chart (sparkline or area) beneath or beside the value. |
| Card Stat | Self-contained in a Card with border/shadow. | Elevated surface with padding, often in a grid of 3–4 cards. |
| Inline Stat | Compact, used within sentences or table cells. | No card wrapper. Value and label on a single line. |
| Comparison Stat | Shows current vs. previous period. | Two values side by side with a delta indicator. |
Size Variants
| Size | Value Font Size | Label Font Size | Use Case |
|---|---|---|---|
| Small | 20–24px | 12–13px | Table cells, dense dashboards, sidebar stats |
| Medium | 28–32px | 14px | Standard dashboard cards |
| Large | 36–48px | 16px | Hero-level KPIs, landing page metrics |
Trend Indicator Styles
- Arrow + Percentage:
↑ 12.3%— Most common, clear and compact - Arrow only:
↑— When the exact delta isn't important - Color-coded value: The value itself turns green/red — Use sparingly, as color alone fails WCAG SC 1.4.1
- Background tint: Subtle green/red background behind the trend — Requires sufficient contrast per Contrast Checker
Properties
Stat Properties
| Property | Type | Default | Description |
|---|---|---|---|
label | string | — | Descriptive text above or below the value (e.g., "Total Revenue") |
value | string | number | — | The primary metric to display |
helpText | string | — | Additional context shown below the value or as a Tooltip |
trend | 'up' | 'down' | 'neutral' | — | Direction of the trend indicator arrow |
trendValue | string | — | Text displayed next to the trend arrow (e.g., "+12.3%") |
icon | ReactNode | — | Leading icon for category context |
size | 'sm' | 'md' | 'lg' | 'md' | Controls value and label font sizes |
formatter | (value: number) => string | — | Custom formatting function for the value |
loading | boolean | false | Displays a Skeleton placeholder |
prefix | string | — | Text/symbol before the value (e.g., "$", "€") |
suffix | string | — | Text/symbol after the value (e.g., "%", "users") |
Important: Never rely solely on color to convey trend direction. Always pair color with an icon (arrow) or text ("+"/"-") to satisfy WCAG SC 1.4.1 (Use of Color). Screen readers need the trend communicated via text, not color.
Token Mappings
Design Token Mappings
| Token Category | Token Example | Stat Usage |
|---|---|---|
| Color – Text | --color-text-primary | Value text color |
| Color – Text Muted | --color-text-secondary | Label and help text |
| Color – Success | --color-success-600 | Positive trend indicator |
| Color – Error | --color-error-600 | Negative trend indicator |
| Color – Surface | --color-surface-elevated | Card stat background |
| Typography – Display | --font-size-2xl, --font-weight-bold | Value text styling |
| Typography – Body | --font-size-sm, --font-weight-medium | Label text styling |
| Typography – Mono | --font-family-mono | Numeric values for tabular alignment |
| Spacing | --space-1, --space-2 | Gap between label, value, and trend |
| Border Radius | --radius-lg | Card stat container rounding |
| Shadow | --shadow-sm | Card stat elevation. Configure via Shadow Tool. |
Use font-variant-numeric: tabular-nums on stat values so digits maintain consistent widths. This prevents layout shifts when values change (e.g., animated counters going from 999 to 1,000).
States
Stat States
| State | Description | Visual Treatment |
|---|---|---|
| Default | Static display of the metric. | Full opacity, normal colors. |
| Loading | Data is being fetched. | Skeleton placeholder matching the value and label dimensions. |
| Error | Data failed to load. | Muted value with an error icon and "Failed to load" text. Provide retry action. |
| Animated | Value is counting up from 0 to the target. | Use CountUp animation with easing. Respect prefers-reduced-motion. |
| Stale | Data is outdated (e.g., cache expired). | Subtle muted overlay or "Last updated X min ago" timestamp. |
| Hover (Card Stat) | Mouse over a clickable stat card. | Slight shadow increase or background tint. Cursor changes to pointer if the stat links somewhere. |
Animation note: Count-up animations are engaging but can be disorienting. Always disable them when prefers-reduced-motion: reduce is active — show the final value immediately instead.
Accessibility
Accessibility Requirements
Semantic Structure:
- Wrap each stat in a
<div>or<figure>with descriptive labeling. For a group of stats, use<dl>(description list) with<dt>for labels and<dd>for values — this is the most semantically correct approach. - Use
aria-labeloraria-labelledbyto associate the value with its label for screen readers.
WCAG Compliance:
- SC 1.4.1 (Use of Color): Trend indicators must not rely on color alone. Pair green/red with directional arrows or "+"/"-" text. Verify trend colors against backgrounds with the Contrast Checker.
- SC 1.4.3 (Contrast – Minimum): Value text (large, bold) typically qualifies for the 3:1 large-text ratio. Label text requires the standard 4.5:1 ratio.
- SC 1.4.11 (Non-text Contrast): Trend arrows and icons need 3:1 contrast against their background.
- SC 1.3.1 (Info and Relationships): Use
<dl>/<dt>/<dd>oraria-labelledbyto programmatically convey that "Total Revenue" and "$1,234,567" are associated. - SC 4.1.2 (Name, Role, Value): If a stat is clickable (links to a detail page), it must have
role="link"or be wrapped in an<a>tag.
Screen Reader Experience: A stat reading "Total Revenue: $1,234,567. Up 12.3% from last month." is ideal — concise and informative. Avoid reading the raw number without context.
Live Updates:
If stat values update in real-time (WebSocket, polling), use aria-live="polite" to announce changes without interrupting the user. Use aria-atomic="true" so the entire stat is re-read, not just the changed digit.
Usage Guidelines
Usage Guidelines
Do:
- Limit dashboard headers to 3–6 stats maximum. Beyond that, cognitive load increases and nothing stands out.
- Use consistent formatting across stats in the same row — all percentages, all currency, or all counts. Mixing formats forces context-switching.
- Place the most important stat first (left in LTR layouts). Eye-tracking shows the first position gets 2–3× more fixations.
- Provide a "Last updated" timestamp or relative time for data-driven stats.
- Use tabular numerals (
font-variant-numeric: tabular-nums) so values align vertically in grids.
Don't:
- Don't display raw database numbers without formatting — "1234567.89" is unreadable; "$1,234,568" is instant.
- Don't use abbreviations without full-value tooltips — "1.2M" is ambiguous without knowing the exact figure.
- Don't make every stat clickable. If a stat links to a detail view, make the interaction obvious with a hover effect or explicit "View details" link.
- Don't animate stats on every re-render. Count-up animations should only fire on initial load, not on data refresh.
- Don't use more than two trend colors (typically green/red). Additional colors (yellow, blue) confuse the directionality signal.
Code Snippets
<!-- Basic Stat -->
<div class="stat">
<dt class="stat-label">Total Revenue</dt>
<dd class="stat-value">$1,234,567</dd>
<dd class="stat-trend stat-trend--up">
<svg class="stat-trend-icon" aria-hidden="true"><!-- arrow up --></svg>
<span>+12.3%</span>
<span class="sr-only">increase from last month</span>
</dd>
</div>
<!-- Stat Group using Description List -->
<dl class="stat-group">
<div class="stat">
<dt class="stat-label">Users</dt>
<dd class="stat-value">84,203</dd>
</div>
<div class="stat">
<dt class="stat-label">Conversion</dt>
<dd class="stat-value">3.24%</dd>
</div>
<div class="stat">
<dt class="stat-label">Revenue</dt>
<dd class="stat-value">$1.2M</dd>
</div>
</dl>
<!-- Stat Card with Icon -->
<div class="stat-card">
<div class="stat-icon" aria-hidden="true">
<svg><!-- users icon --></svg>
</div>
<dl>
<dt class="stat-label">Active Users</dt>
<dd class="stat-value">12,847</dd>
<dd class="stat-help">Last 30 days</dd>
</dl>
</div>// Basic Stat Component
interface StatProps {
label: string;
value: string | number;
trend?: 'up' | 'down' | 'neutral';
trendValue?: string;
helpText?: string;
icon?: React.ReactNode;
size?: 'sm' | 'md' | 'lg';
prefix?: string;
suffix?: string;
loading?: boolean;
formatter?: (value: number) => string;
}
function Stat({
label,
value,
trend,
trendValue,
helpText,
icon,
size = 'md',
prefix,
suffix,
loading,
formatter,
}: StatProps) {
const formattedValue = typeof value === 'number' && formatter
? formatter(value)
: value;
if (loading) {
return (
<div className={`stat stat--${size}`}>
<div className="stat-label skeleton" />
<div className="stat-value skeleton" />
</div>
);
}
return (
<div className={`stat stat--${size}`}>
{icon && <div className="stat-icon" aria-hidden="true">{icon}</div>}
<dt className="stat-label">{label}</dt>
<dd className="stat-value" style={{ fontVariantNumeric: 'tabular-nums' }}>
{prefix}{formattedValue}{suffix}
</dd>
{trend && trendValue && (
<dd className={`stat-trend stat-trend--${trend}`}>
<TrendIcon direction={trend} />
<span>{trendValue}</span>
</dd>
)}
{helpText && <dd className="stat-help">{helpText}</dd>}
</div>
);
}
// Stat Group
function StatGroup({ children }: { children: React.ReactNode }) {
return <dl className="stat-group">{children}</dl>;
}
// Usage
<StatGroup>
<Stat label="Total Revenue" value={1234567} prefix="$"
formatter={(v) => v.toLocaleString()} trend="up" trendValue="+12.3%" />
<Stat label="Active Users" value="84,203" trend="up" trendValue="+5.7%" />
<Stat label="Bounce Rate" value="32.1%" trend="down" trendValue="-2.4%"
helpText="Last 30 days" />
</StatGroup>Design Systems
Material Design 3 does not provide a dedicated Stat component. Developers typically compose stats from Typography, Card, and Stack components. MUI's Typography with variant="h3" for the value and variant="body2" for the label is the standard pattern. Community libraries like @mui/x-charts offer sparkline components that pair well with custom stat cards.
Ant Design provides a Statistic component with title, value, prefix, suffix, precision (decimal places), formatter, valueStyle, and a Statistic.Countdown subcomponent for timer displays. Ant's implementation handles number formatting automatically using toLocaleString() and supports groupSeparator customization. The formatter prop accepts the value and renders a ReactNode, enabling custom formatting like colored text or embedded icons.
Chakra UI provides a Stat compound component with StatLabel, StatNumber, StatHelpText, StatArrow (renders up/down arrow with appropriate green/red color), and StatGroup (flex container with spacing). Chakra's StatArrow automatically sets aria-label="increased" or aria-label="decreased" — a thoughtful accessibility detail. The StatGroup uses flexbox with flexWrap="wrap" for responsive layouts.
Radix UI and Headless UI do not provide Stat primitives, as the component is primarily presentational with no complex interaction patterns requiring headless abstraction.
Tremor (React library for dashboards) provides an excellent Metric and BadgeDelta combination that serves as a stat component with built-in trend indicators, sparklines via SparkAreaChart, and responsive card layouts. Tremor's approach is dashboard-first and worth studying for stat design patterns.