Skip to content

FRD-038: Quick Stats Grid

FieldValue
OwnerDavid Holmes
StatusDraft
Last Updated2026-05-26
Target Releasev2.0.0
TypeWidget
SizeS
PriorityP1 — SaaS Widget

Document Summary

Build a Quick Stats Grid widget that arranges MetricCard components in a responsive grid with configurable column count. It complements the existing metric-card.tsx (individual cards) and kpi-summary-strip.tsx (horizontal strip) by providing a grid layout suitable for dashboard overviews, with loading states and realistic Storybook stories.


Introduction

Overview

The design system provides MetricCard for individual KPI display and KpiSummaryStrip for horizontal arrangements of 1-6 metrics. However, dashboards often need a grid layout with more than 6 metrics, configurable column counts, and loading states. KpiSummaryStrip is specifically designed for a horizontal strip and auto-sizes columns based on tile count (up to 6). A QuickStatsGrid widget provides a more flexible grid layout for arbitrary metric counts with explicit column control.

Goals

  • Provide a QuickStatsGrid widget that renders MetricCard components in a responsive CSS grid.
  • Support configurable column count (default auto-responsive) with explicit override.
  • Handle loading state with skeleton cards matching the expected card layout.
  • Ship with Storybook stories using realistic dashboard data.

Non-Goals

  • Replacing KpiSummaryStrip (it remains the right choice for horizontal 1-6 metric strips).
  • Chart/graph rendering within cards (MetricCard already supports sparklines).
  • Drag-to-rearrange card positions.

Scope

In Scope

ItemDescription
QuickStatsGrid widgetCSS grid container for MetricCard components.
Column configurationcolumns prop for explicit column count; responsive default (1→2→3→4 across breakpoints).
Loading stateSkeleton cards matching the expected grid layout.
MetricCard pass-throughEach stat config maps to MetricCard props (label, value, delta, sparkline, icon, tone).
Card variantsSupport for MetricCard variant (default, outlined, filled) and tone applied uniformly or per card.
Stories with realistic dataStorybook stories demonstrating SaaS dashboard, e-commerce, and DevOps metric grids.
Unit testsTests for responsive breakpoint behavior, loading state rendering, and prop pass-through.

Out of Scope

ItemReason
Chart integrationMetricCard already supports sparklines; complex charts belong in separate chart components.
Card reorderingDrag-and-drop is a separate concern with high complexity.
Server-side data fetchingConsumer provides the stat data; loading state is driven by consumer.
Animations between statesCards transition via standard motion tokens; no complex enter/exit orchestration.

Users and Pain Points

UserPain Point
Dashboard developersLayout MetricCards manually with CSS grid for every dashboard; inconsistent responsive breakpoints.
Product designersNo standard grid layout for metric cards; designs specify grids that developers implement differently.
Developers building overview pagesKpiSummaryStrip caps at 6 tiles and is horizontal-strip only; no grid option for 8-12 metrics.

Definitions

TermDefinition
StatA single metric displayed in a MetricCard (label, value, delta, sparkline).
Column countThe number of MetricCards per row in the grid.
Responsive defaultA grid that automatically adjusts column count based on viewport width.

Current State

  • MetricCard (src/components/ui/metric-card.tsx): Individual metric display with label, value, delta, deltaDirection, sparkline, icon, trend. Supports variant (default, outlined, filled) and tone (themedSurfaceToneClasses).
  • KpiSummaryStrip (src/components/widgets/kpi-summary-strip.tsx): Horizontal strip of 1-6 MetricCards with auto-responsive grid sizing. Accepts stats: KpiConfig[], variant. Limited to 6 tiles and horizontal layout.
  • No general-purpose metric grid exists for 6+ metrics or explicit column control.

Proposed Solution

Build src/components/widgets/quick-stats-grid.tsx:

Grid Layout

A CSS grid container using Tailwind grid classes. The default responsive behavior:

  • Mobile (< 640px): 1 column
  • Small (640px+): 2 columns
  • Medium (768px+): 3 columns
  • Large (1024px+): 4 columns

The columns prop overrides this with an explicit column count at all breakpoints. A minCardWidth prop can alternatively set grid-template-columns: repeat(auto-fill, minmax(minCardWidth, 1fr)) for a fluid approach.

Stat Configuration

Reuse the KpiConfig interface from KpiSummaryStrip for consistency. Each stat maps directly to MetricCard props.

Loading State

When loading is true, render skeleton cards. The skeleton count matches either stats.length (if data has been loaded before) or a skeletonCount prop. Each skeleton card matches the MetricCard dimensions with shimmer animation.

Card Variant

variant and tone can be applied globally (all cards) or per-stat (each StatConfig can override).


Requirements

Requirement Priorities

  • Must Have: Grid layout with MetricCards, responsive default, loading state.
  • Should Have: Explicit column count, per-stat tone override, gap configuration.
  • Could Have: Fluid minCardWidth mode, card click handler, skeleton count override.

Functional Requirements

IDRequirementPriority
FR-01Widget renders MetricCards in a CSS grid container.Must
FR-02Responsive default: 1 col mobile, 2 col sm, 3 col md, 4 col lg.Must
FR-03columns prop overrides responsive behavior with explicit column count.Should
FR-04Each stat config maps to MetricCard props (label, value, delta, deltaDirection, sparkline, icon).Must
FR-05variant prop applies to all cards; per-stat variant override is supported.Should
FR-06tone prop applies to all cards; per-stat tone override is supported.Should
FR-07Loading state renders skeleton cards matching grid layout.Must
FR-08skeletonCount prop sets the number of skeleton cards during loading.Should
FR-09gap prop configures the grid gap (default gap-4).Could
FR-10onCardClick callback fires with the stat config when a card is clicked.Could

Non-Functional Requirements

IDRequirementTarget
NFR-01Renders 12 stat cards in < 20ms.React Profiler measurement.
NFR-02Bundle size< 2 KB gzipped (excluding MetricCard dependency).
NFR-03Responsive layout shiftNo cumulative layout shift during responsive transitions.
NFR-04Dark modeFull token-based dark mode support (inherited from MetricCard).
NFR-05Skeleton animationUses existing Skeleton component shimmer.

API/Interface Requirements

interface StatConfig {
id: string;
label: string;
value: string | number;
delta?: string;
deltaDirection?: "up" | "down" | "neutral";
description?: string;
sparkline?: number[];
icon?: ReactNode;
variant?: "default" | "outlined" | "filled";
tone?: string;
}
interface QuickStatsGridProps {
stats: StatConfig[];
loading?: boolean;
skeletonCount?: number; // default: stats.length or 4
columns?: number; // explicit override; omit for responsive
minCardWidth?: string; // e.g. "240px" for fluid grid
variant?: "default" | "outlined" | "filled";
tone?: string;
gap?: string; // Tailwind gap class, default "gap-4"
onCardClick?: (stat: StatConfig) => void;
className?: string;
}

Accessibility Requirements

IDRequirement
A11Y-01Grid container uses role="list" or semantic &lt;ul&gt; with &lt;li&gt; wrappers for each card.
A11Y-02Each MetricCard’s label, value, and delta are readable by screen readers in logical order.
A11Y-03If onCardClick is provided, cards are focusable and activatable via Enter/Space.
A11Y-04Skeleton loading state uses aria-busy="true" on the container.
A11Y-05Sparklines include aria-hidden="true" (decorative) with metric values as the accessible content.
A11Y-06All components pass axe-core automated checks with zero violations.

Content and Documentation Requirements

IDRequirement
DOC-01Storybook docs page with usage guidelines, prop table, and interactive examples.
DOC-02Stories with realistic data: SaaS dashboard (MRR, churn, users, NPS), e-commerce (revenue, orders, AOV, conversion), DevOps (uptime, deploy frequency, MTTR, error rate).
DOC-03Comparison guide: when to use QuickStatsGrid vs. KpiSummaryStrip.

Dependencies

DependencyTypeRisk
src/components/ui/metric-card.tsxInternalLow — individual card component.
src/components/ui/skeleton.tsxInternalLow — loading state.
src/components/widgets/kpi-summary-strip.tsxInternalLow — API consistency reference (KpiConfig).

Risks and Tradeoffs

RiskLikelihoodImpactMitigation
Overlap with KpiSummaryStrip causes confusion on which to useMediumLowDocument clear guidance: KpiSummaryStrip for 1-6 horizontal strip; QuickStatsGrid for grid layouts with 4+ metrics.
Too many cards (20+) makes the grid unwieldyLowLowDocument recommended max of 12-16 cards; beyond that, use tabs or sections.
StatConfig duplicates KpiConfig with minor differencesMediumLowExtend KpiConfig or alias it; avoid divergence.

Open Questions

#QuestionOwnerStatus
OQ-01Should StatConfig extend KpiConfig directly or be a new interface?David HolmesOpen
OQ-02Should the grid support a “compact” variant with smaller cards for dense dashboards?David HolmesOpen
OQ-03Should the widget support metric grouping (sections within the grid)?David HolmesOpen

Acceptance Criteria

#Criterion
AC-01Widget renders MetricCards in a responsive CSS grid (1→2→3→4 columns across breakpoints).
AC-02columns prop overrides responsive behavior with a fixed column count.
AC-03Loading state renders skeleton cards with shimmer animation.
AC-04Each stat config maps correctly to MetricCard props (label, value, delta, sparkline, icon).
AC-05Per-stat variant and tone overrides work alongside global settings.
AC-06All components pass axe-core checks with zero violations.
AC-07Storybook stories exist with 3+ realistic data scenarios.
AC-08pnpm typecheck and pnpm vitest run --project unit pass with zero errors.

LLM Handoff Instructions

When implementing this FRD:

  1. Create src/components/widgets/quick-stats-grid.tsx.
  2. Grid layout: Use Tailwind CSS grid classes. Default: grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4. If columns prop is set, use grid-cols-{columns} (or inline style for arbitrary values).
  3. Card rendering: Map over stats array and render a MetricCard for each. Pass through label, value, delta, deltaDirection, description, sparkline, icon. Apply global variant and tone, then per-stat overrides.
  4. Loading state: When loading is true, render skeletonCount skeleton cards. Use the existing Skeleton component with rounded-xl and matching MetricCard dimensions (h-[120px] or similar).
  5. Click handler: If onCardClick is provided, wrap each MetricCard in a &lt;button&gt; or add onClick and tabIndex={0} for keyboard access.
  6. Stories in src/components/widgets/quick-stats-grid.stories.tsx. Create realistic scenarios:
    • SaaS Dashboard: MRR ($124K, +8.4%), Active Users (2,847, +12%), Churn Rate (2.1%, -0.3%), NPS (72, +5).
    • E-commerce: Revenue (89K),Orders(1,204),AOV(89K), Orders (1,204), AOV (74), Conversion (3.2%).
    • DevOps: Uptime (99.97%), Deploy Frequency (12/day), MTTR (14min), Error Rate (0.02%).
  7. Tests in src/components/widgets/quick-stats-grid.test.tsx. Test card rendering, loading state, column override, click handler.

Key files:

  • src/components/ui/metric-card.tsx — individual card props and variants.
  • src/components/widgets/kpi-summary-strip.tsx — KpiConfig interface and strip pattern.
  • src/components/ui/skeleton.tsx — loading placeholder.

Decision Log

DateDecisionRationale
2026-05-26Separate widget from KpiSummaryStrip rather than extending it.KpiSummaryStrip is optimized for horizontal strips of 1-6; grid layout has different responsive needs.
2026-05-26Responsive default rather than requiring explicit column count.Most dashboards benefit from auto-responsive; explicit override available for specific layouts.
2026-05-26Reuse MetricCard rather than building a new card component.MetricCard already supports all needed features (delta, sparkline, icon, tone, variant).

Document History

VersionDateAuthorChanges
0.12026-05-26David HolmesInitial draft.