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 samplesp502195msp752694msp953235ms
- INP240 samplesp50179msp75243msp95313ms
- CLS240 samplesp500.08p750.12p950.17
Customize
Data
240
2200ms
Histogram
24
Chrome
Installation
npx shadcn@latest add https://craftbits.dev/r/rum-widget.jsonUsage
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+descriptionwithcb-labelstyling. Hidden when both are absent. - Metric row. Label on the left, sample count on the right.
- Histogram.
bins(default24) bars between the smallest and largest sample. Bars usescaleYonly 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-ratingattribute.
Understanding the component
- Samples to percentiles. The widget filters by
metric.id, sorts the values, and reads p50 / p75 / p95 via linear interpolation. Pre-aggregatedp50/p75/p95props skip the calculation. - Sliding window. Pass
windowMsto drop samples older thannow - windowMs.nowdefaults to the most recent sample timestamp — noDate.now()at module scope, safe under SSR. - Thresholds drive the rating. A metric with
thresholds: { good, poor }paints values<= goodgreen,>= poorred, and the band between yellow. Drop the thresholds to render every cell neutral. - Histogram entry. Bars use a short
SPRINGS.dampedscaleY —prefers-reduced-motionshort-circuits to instant. - 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
| Prop | Type | Default | Description |
|---|---|---|---|
metrics | RumWidgetMetric[] | required | One row per metric. |
samples | RumWidgetSample[] | required | Flat bag of beacons. |
title | ReactNode | — | Optional headline. |
description | ReactNode | — | Optional sub-headline. |
windowMs | number | — | Sliding window — exclude older samples. |
now | number | most recent sample | Reference time for the sliding window. |
bins | number | 24 | Histogram bucket count. |
hideChart | boolean | false | Hide histograms. |
headingAs | 'h2' | 'h3' | 'h4' | 'h3' | Tag for the title element. |
aria-label | string | 'RUM widget' | Region label when no title is set. |
className | string | — | Merged onto the root via cn(). |
RumWidgetMetric
| Field | Type | Description |
|---|---|---|
id | string | Joins with RumWidgetSample.metric. |
label | ReactNode | Visible row label. |
unit | string | Optional unit suffix. |
thresholds | { good, poor } | Optional rating cutoffs. |
p50 / p75 / p95 | number | Pre-aggregated percentiles. |
format | (value, unit) => string | Custom value formatter. |
RumWidgetSample
| Field | Type | Description |
|---|---|---|
timestamp | number | Beacon time in ms since epoch. |
metric | string | Matches a RumWidgetMetric.id. |
value | number | Observed value. |
Accessibility
- The panel root is a
<section>carryingdata-cb-edu="rum-widget". - The metric list is
role="list"withrole="listitem"children. - Histograms are
aria-hidden="true"— the rating cells carry the legible values anddata-cb-ratingattribute 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 pulledmetrics,enabledOptimizations, andactiveProfileout of aPerfContextand 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.