CWV Gauges

A Core Web Vitals dashboard row — one radial gauge per metric, each pinned to a [good, poor] threshold pair. The component does the bucketing (good / warn / bad), the sweep math, and the tone. Generic enough for any threshold-rated metric, not just CWV.

Preview
1.8sLCP
GoodGood <= 2.5sLargest Contentful Paint
220msINP
Needs workGood <= 200msInteraction to Next Paint
0.14CLS
Needs workGood <= 0.1Cumulative Layout Shift
Customize
Options

Installation

npx shadcn@latest add https://craftbits.dev/r/cwv-gauges.json

Usage

import { CwvGauges } from "@craft-bits/core";
 
<CwvGauges
  metrics={[
    { id: "lcp", label: "LCP", value: 1.8, unit: "s", thresholds: [2.5, 4.0] },
    { id: "inp", label: "INP", value: 220, unit: "ms", thresholds: [200, 500] },
    { id: "cls", label: "CLS", value: 0.14, thresholds: [0.1, 0.25] },
  ]}
/>

Highlight the gauge whose value just changed:

<CwvGauges metrics={metrics} pulseKey="lcp" />

Invert the rating direction for "higher is better" metrics:

<CwvGauges
  metrics={[
    {
      id: "hit",
      label: "Hit rate",
      value: 92,
      unit: "%",
      thresholds: [90, 70],
      higherIsBetter: true,
    },
  ]}
/>

Anatomy

  • Bucketing — each metric carries thresholds: [good, poor]. For lower-is-better metrics, value <= good is good, value <= poor is warn, anything else is bad. higherIsBetter flips the comparison.
  • Sweep — the radial arc length is a normalised fraction of value / poor, clamped to [0.04, 0.95] so the ring is always visible but never closes a full circle.
  • Tone — the bucket maps to cb-success / cb-warning / cb-error on the sweep stroke, the value text, and the rating chip.
  • Pulse — when pulseKey matches a metric's id, that gauge scales briefly — a quiet way to spotlight a value that just changed.

Props

PropTypeDefaultDescription
metricsCwvGaugeMetric[]requiredOrdered gauges.
pulseKeystring | nullnullMetric id to briefly highlight.
aria-labelstring'Core Web Vitals'Accessible group label.
minTileWidthstring'11rem'Minimum gauge tile width for the auto-fit grid.
classNamestringMerged onto the root via cn().

CwvGaugeMetric

FieldTypeDescription
idstringStable identifier — used as the React key and matched against pulseKey.
labelReactNodeShort visible label (e.g. "LCP").
valuenumberRaw numeric value plotted on the gauge.
unitstringOptional unit suffix.
thresholds[number, number][goodCutoff, poorCutoff] — same units as value.
descriptionReactNodeOptional subtitle under the rating chip.
format(value, unit) => stringOverride the value rendering.
higherIsBetterbooleanInvert the comparison for metrics where larger is better.

Accessibility

  • The row root is a role="group" with an aria-label ("Core Web Vitals" by default).
  • Each gauge is a role="meter" with aria-valuenow / aria-valuemin / aria-valuemax so screen readers announce the value and its scale.
  • The aria-label on each meter composes label, formatted value, and rating bucket.
  • Reduced-motion users get a static sweep — no animated dash offset, no pulse.

Credits

  • Extracted from: terminal-dreams (src/components/frontend-design/perf-cwv/ui/CwvGauges.tsx). The original was bound to a CWV simulator engine with hard-coded formatLcp / rateLcp / etc.; this primitive drops the engine coupling and lets the consumer name, format, and threshold any metric.