RUM Widget

A compact real-user-monitoring panel. Drop in a flat bag of beacons — { timestamp, metric, value } — and the widget renders one row per metric: a distribution histogram, the p50 / p75 / p95 lines drawn straight over the chart, and three rating cells underneath. Percentiles are computed client-side via linear interpolation; pre-aggregated values from the server override the calculation when you pass them.

Preview

Field data — 24h

p75 is the headline; p95 is the tail.

  • LCP240 samples
    p502195ms
    p752694ms
    p953235ms
  • INP240 samples
    p50179ms
    p75243ms
    p95313ms
  • CLS240 samples
    p500.08
    p750.12
    p950.17
Customize
Data
240
2200ms
Histogram
24
Chrome

Installation

npx shadcn@latest add https://craftbits.dev/r/rum-widget.json

Usage

import { RumWidget } from "@craft-bits/core";
 
<RumWidget
  metrics={[
    { id: "lcp", label: "LCP", unit: "ms", thresholds: { good: 2500, poor: 4000 } },
    { id: "inp", label: "INP", unit: "ms", thresholds: { good: 200, poor: 500 } },
    { id: "cls", label: "CLS", thresholds: { good: 0.1, poor: 0.25 } },
  ]}
  samples={beacons}
/>

Server-bucketed percentiles — skip client-side calculation:

<RumWidget
  metrics={[
    { id: "ttfb", label: "TTFB", unit: "ms", p50: 180, p75: 320, p95: 740 },
  ]}
  samples={[]}
/>

Sliding window — keep only the last hour of beacons:

<RumWidget
  metrics={metrics}
  samples={beacons}
  windowMs={60 * 60 * 1000}
  now={Date.now()}
/>

Anatomy

  • Header. Optional title + description with cb-label styling. Hidden when both are absent.
  • Metric row. Label on the left, sample count on the right.
  • Histogram. bins (default 24) bars between the smallest and largest sample. Bars use scaleY only so animation stays on the compositor.
  • Percentile lines. p50 / p75 / p95 as 1px verticals over the histogram, coloured by their rating.
  • Rating cells. Three tiles under the chart with the formatted percentile value and a data-cb-rating attribute.

Understanding the component

  1. Samples to percentiles. The widget filters by metric.id, sorts the values, and reads p50 / p75 / p95 via linear interpolation. Pre-aggregated p50 / p75 / p95 props skip the calculation.
  2. Sliding window. Pass windowMs to drop samples older than now - windowMs. now defaults to the most recent sample timestamp — no Date.now() at module scope, safe under SSR.
  3. Thresholds drive the rating. A metric with thresholds: { good, poor } paints values <= good green, >= poor red, and the band between yellow. Drop the thresholds to render every cell neutral.
  4. Histogram entry. Bars use a short SPRINGS.damped scaleY — prefers-reduced-motion short-circuits to instant.
  5. No project chrome. The original lab widget bundled journey strip, baseline-vs-current waterfall overlay, alerts, and recap copy. The library version keeps only the RUM primitive — alerts and recap belong in the consumer.

Props

PropTypeDefaultDescription
metricsRumWidgetMetric[]requiredOne row per metric.
samplesRumWidgetSample[]requiredFlat bag of beacons.
titleReactNodeOptional headline.
descriptionReactNodeOptional sub-headline.
windowMsnumberSliding window — exclude older samples.
nownumbermost recent sampleReference time for the sliding window.
binsnumber24Histogram bucket count.
hideChartbooleanfalseHide histograms.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
aria-labelstring'RUM widget'Region label when no title is set.
classNamestringMerged onto the root via cn().

RumWidgetMetric

FieldTypeDescription
idstringJoins with RumWidgetSample.metric.
labelReactNodeVisible row label.
unitstringOptional unit suffix.
thresholds{ good, poor }Optional rating cutoffs.
p50 / p75 / p95numberPre-aggregated percentiles.
format(value, unit) => stringCustom value formatter.

RumWidgetSample

FieldTypeDescription
timestampnumberBeacon time in ms since epoch.
metricstringMatches a RumWidgetMetric.id.
valuenumberObserved value.

Accessibility

  • The panel root is a <section> carrying data-cb-edu="rum-widget".
  • The metric list is role="list" with role="listitem" children.
  • Histograms are aria-hidden="true" — the rating cells carry the legible values and data-cb-rating attribute so assistive tech still reads the percentiles.
  • Reduced-motion users get static bars — no scaleY entry animation.

Credits

  • Extracted from: terminal-dreams (src/components/frontend-design/sdp-web-performance/ui/RUMWidget.tsx). The original pulled metrics, enabledOptimizations, and activeProfile out of a PerfContext and rendered a journey strip, a baseline-vs-current waterfall overlay, a fixed CWV grid, an alert panel, and explanatory copy — all glued to one CWV lab. This rewrite keeps only the RUM primitive (samples to histogram + percentiles) and hands every shape decision back to the consumer.