Loading…
Loading…
Displays a numerical value with a label, often used in dashboards and summaries.
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:
When NOT to use a Stat:
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.
| 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 | 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 |
↑ 12.3% — Most common, clear and compact↑ — When the exact delta isn't important| 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 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).
| 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.
Semantic Structure:
<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.aria-label or aria-labelledby to associate the value with its label for screen readers.WCAG Compliance:
<dl>/<dt>/<dd> or aria-labelledby to programmatically convey that "Total Revenue" and "$1,234,567" are associated.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.
Do:
font-variant-numeric: tabular-nums) so values align vertically in grids.Don't:
<!-- 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>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.