Skip to content

Stat

Displays a numerical value with a label, often used in dashboards and summaries.

  • Metric
  • KPI
  • Statistic

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

VariantPurposeVisual Treatment
DefaultStandard KPI display.Label above or below a large value. Minimal decoration.
With TrendShows direction of change (up/down).Includes an arrow icon and percentage delta, colored green (positive) or red (negative).
With IconProvides category context at a glance.Leading icon (e.g., dollar sign, users icon) in a colored circle or square.
With SparklineAdds micro-trend context without a full chart.Small inline chart (sparkline or area) beneath or beside the value.
Card StatSelf-contained in a Card with border/shadow.Elevated surface with padding, often in a grid of 3–4 cards.
Inline StatCompact, used within sentences or table cells.No card wrapper. Value and label on a single line.
Comparison StatShows current vs. previous period.Two values side by side with a delta indicator.

Size Variants

SizeValue Font SizeLabel Font SizeUse Case
Small20–24px12–13pxTable cells, dense dashboards, sidebar stats
Medium28–32px14pxStandard dashboard cards
Large36–48px16pxHero-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

PropertyTypeDefaultDescription
labelstring—Descriptive text above or below the value (e.g., "Total Revenue")
valuestring | number—The primary metric to display
helpTextstring—Additional context shown below the value or as a Tooltip
trend'up' | 'down' | 'neutral'—Direction of the trend indicator arrow
trendValuestring—Text displayed next to the trend arrow (e.g., "+12.3%")
iconReactNode—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
loadingbooleanfalseDisplays a Skeleton placeholder
prefixstring—Text/symbol before the value (e.g., "$", "€")
suffixstring—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 CategoryToken ExampleStat Usage
Color – Text--color-text-primaryValue text color
Color – Text Muted--color-text-secondaryLabel and help text
Color – Success--color-success-600Positive trend indicator
Color – Error--color-error-600Negative trend indicator
Color – Surface--color-surface-elevatedCard stat background
Typography – Display--font-size-2xl, --font-weight-boldValue text styling
Typography – Body--font-size-sm, --font-weight-mediumLabel text styling
Typography – Mono--font-family-monoNumeric values for tabular alignment
Spacing--space-1, --space-2Gap between label, value, and trend
Border Radius--radius-lgCard stat container rounding
Shadow--shadow-smCard 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

StateDescriptionVisual Treatment
DefaultStatic display of the metric.Full opacity, normal colors.
LoadingData is being fetched.Skeleton placeholder matching the value and label dimensions.
ErrorData failed to load.Muted value with an error icon and "Failed to load" text. Provide retry action.
AnimatedValue is counting up from 0 to the target.Use CountUp animation with easing. Respect prefers-reduced-motion.
StaleData 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-label or aria-labelledby to 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> or aria-labelledby to 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

html
<!-- 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>
tsx
// 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.

Data DisplayMetricKPIStatistic