MetricCard

Analytics-surface KPI card with 3 layout variants (basic / split / glance) selected via discriminated-union props. Consolidates 5 audit candidates into one compound. Use `basic` for single KPIs scanned in rows, `split` for service-line breakdowns inside one card, `glance` for grids of micro-KPIs with per-cell context.

Spec · from metadata

When to use

  • Dashboard KPI rows — multiple basic-layout MetricCards across the top of an admin dashboard
  • Service-line aggregates — one split-layout card showing Medical/Behavioral/Psychiatry subsegments
  • At-a-glance summaries — one glance-layout card with a 2×N grid of small metrics + context lines
  • Any analytics surface where a number needs a label + optional drill-in affordance

When not to use

  • Status indicators on table rows — use Badge with status tone instead
  • Counts attached to nav items — use SidebarMenuBadge or CountedTabs
  • Large hero numbers in marketing pages — build a custom layout; MetricCard is dashboard chrome, not marketing
  • When you need to compare two values side-by-side with up/down arrows and a delta — that's a different pattern (TBD; build locally)

Anti-patterns

Avoid<MetricCard
  layout="basic"
  label="Visits in progress"
  value="23"
  breakdown={[
    { id: "medical", label: "Medical", value: 12 },
    { id: "behavioral", label: "Behavioral", value: 11 },
  ]}
/>
Prefer<MetricCard
  layout="split"
  label="Visits in progress"
  breakdown={[
    { id: "medical", label: "Medical", value: 12 },
    { id: "behavioral", label: "Behavioral", value: 11 },
  ]}
/>

The basic layout doesn't accept a breakdown prop (TypeScript will refuse). Use the split layout when you have a per-sub-metric breakdown — the discriminated union is what makes the layout-selection rule unambiguous.

Avoid<MetricCard
  layout="glance"
  label="At a Glance"           // Title Case
  cells={[
    { id: "active", label: "Active", value: "18" },   // Sentence-case label
  ]}
/>
Prefer<MetricCard
  layout="glance"
  label="At a glance"           // Sentence case for the card label
  cells={[
    { id: "active", label: "ACTIVE", value: "18" },   // UPPERCASE for cell labels (glance convention)
  ]}
/>

Glance layout uses UPPERCASE for cell labels (audit-confirmed convention: tight + scannable in a dense grid). The card-level label stays sentence case like everywhere else (helix-copy-patterns.md).

Avoid<MetricCard layout="basic" label="Status" value={<Badge>Active</Badge>} />
Prefer<Badge variant="defaultSage">Active</Badge>

MetricCard is for numeric KPIs in analytics surfaces. A single status indicator is just a Badge — don't wrap it in a card unless you genuinely need the dashboard chrome.

Accessibility

  • Min touch target48px
  • Screen readerIcons are aria-hidden (decorative). Glance tooltip triggers have aria-label='More info'. The drill-in button has visible text 'More' so no additional aria-label needed.
  • ContrastCard uses card-foreground on card bg (WCAG AA). FilledIcon chip tones use accent (info default) + the Badge outline hues at the -200 fill stop with solid tone icons, plus tc-light-sand-100 (neutral) — all AA-tuned in the palette.

Token bindings

TokenCategoryUsage
cardcolorCard surface (from the Card primitive)
card-foregroundcolorDefault text color inherited from the Card primitive
foregroundcolorMetric value text (text-foreground)
muted-foregroundcolorLabel, subtitle, context, and breakdown text
accentcolorFilledIcon chip fill for iconTone="info"
mutedcolorFilledIcon chip fill for iconTone="neutral" — MetricCard's default when iconTone is unset
tc-sage-100colorFilledIcon chip fill for iconTone="success" (tc-sage-700 in dark)
tc-orange-100colorFilledIcon chip fill for iconTone="warning" (tc-orange-700 in dark)
tc-berry-100colorFilledIcon chip fill for iconTone="destructive" (tc-berry-700 in dark)
text-sm / text-3xl / text-2xl / text-xl / text-xstypographyLabel / basic value / glance value / split value / subtitle+context
4 (16px) / 3 (12px) + 6 (24px) / 1 (4px)spacingCard padding / grid gaps (split 12px, glance 24×16px) / label-to-value gap
Recent changes
  • new

    Compound components join the catalog

    2026-07-09

[!NOTE] Compound component — an opinionated composition of Helix primitives. Composes: Card, Tooltip.

Import

import { MetricCard } from "@timelycare/helix-ui"

Usage

// basic — most common; scanning rows
<MetricCard
  layout="basic"
  label="Total visits"
  value="1,284"
  icon={<Activity />}
  iconTone="info"
  subtitle="Last 30 days"
  onDrillIn={() => router.push('/reports/visits')}
/>

// split — service-line breakdown in one card
<MetricCard
  layout="split"
  label="Visits in progress"
  icon={<Activity />}
  iconTone="info"
  breakdown={[
    { id: "medical",    label: "Medical",    value: 12, icon: <Stethoscope /> },
    { id: "behavioral", label: "Behavioral", value: 8,  icon: <Brain /> },
    { id: "psychiatry", label: "Psychiatry", value: 3,  icon: <PillBottle /> },
  ]}
  onDrillIn={() => router.push('/visits/in-progress')}
/>

// glance — 2×N micro-KPI grid with optional tooltips
<MetricCard
  layout="glance"
  label="At a glance"
  cells={[
    { id: "active",   label: "ACTIVE",   value: "18", context: "across 18 campaigns" },
    { id: "students", label: "STUDENTS", value: "11", context: "of 25 students", tooltip: "Students with at least one campaign interaction this week" },
    { id: "responses", label: "RESPONSES", value: "342" },
    { id: "rate",     label: "RATE",     value: "44%", context: "↑ 6% vs last week" },
  ]}
/>