Nasaq

WebVitalsPage

A web vitals report - a Core Web Vitals verdict, gauges for LCP, INP, CLS, FCP and TTFB with distributions, a trend chart with Google's limits, and pages to fix.

PreviewOpen ↗

Code

import { WebVitalsPage } from "@fadymondy/nasaq/web";import { useState } from "react";export function Vitals({ service, data }: { service: React.ComponentProps<typeof WebVitalsPage>["service"]; data?: React.ComponentProps<typeof WebVitalsPage>["data"] }) {  const [period, setPeriod] = useState(28);  return <WebVitalsPage service={service} data={data} site="nasaq.dev" period={period} onPeriodChange={setPeriod} onConnect={async () => {}} onDisconnect={async () => {}} />;}

Monitoring · beta

Live examples and controls: WebVitalsPage in the lab.

Install

npx shadcn@latest add https://docs.nasaqui.com/r/web-vitals-page.json

The web vitals page opens with a verdict (passes Core Web Vitals only when LCP, INP and CLS are all good at the 75th percentile), then a WebVitalGauge per metric with the distribution of page loads, a TimeSeriesPanel for the selected metric with Google's good and poor limits drawn, and a table of the busiest pages rated per metric. A mobile / desktop switch appears when you pass device and onDeviceChange. Until connected it shows AnalyticsConnect.

When to use

  • Real-user performance reporting from CrUX or your own RUM.
  • Deciding which pages to fix first.

When not to use

  • Server-side latency: use ApmPage.
  • One-off Lighthouse audits.

Import

import { WebVitalsPage } from "@fadymondy/nasaq/web";

Quick start

import { WebVitalsPage } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Vitals({ service, data }: { service: React.ComponentProps<typeof WebVitalsPage>["service"]; data?: React.ComponentProps<typeof WebVitalsPage>["data"] }) {
  const [period, setPeriod] = useState(28);
  return <WebVitalsPage service={service} data={data} site="nasaq.dev" period={period} onPeriodChange={setPeriod} onConnect={async () => {}} onDisconnect={async () => {}} />;
}

Anatomy

WebVitalsPage          AnalyticsPageFrame
  verdict card         passes / does not pass / not enough data
  WebVitalGaugeGrid    five selectable gauges
  TimeSeriesPanel      selected metric + good/poor lines
  pages table          LCP, INP, CLS per page, rated

API

WebVitalsPage

PropTypeDefaultDescription
serviceIntegrationServicerequiredThe source and its state; anything but connected shows the connect screen.
data…DatanoneThe report. While it is undefined the page shows skeletons.
period / onPeriodChangenumber / (n) => voidrequiredThe reporting period in days (hours for the APM page).
loadingbooleanfalseSkeletons.
error / onRetryReactNode / () => voidnoneShows an error with a retry button.
onRefresh / refreshing / updatedAt() => Promise / boolean / datenoneRefresh button and the updated time.
onConnect / onDisconnect / onSelectAccountasync callbacksrequiredOAuth and account picker hooks; the host does the real work.
classNamestringnoneExtra classes on the root.
sitestringnoneThe origin, for the subtitle.
device / onDeviceChange"mobile" | "desktop" / (device) => voidnoneShows the device switch.
metric / onMetricChangeWebVitalId / (metric) => voidLCPThe charted metric, controlled or not.
onPageClick(row) => voidnoneMakes page rows clickable.
labels / frameLabelsPartial<…>nonePage strings and the shared header strings.

WebVitalsData

vitals (per metric: { p75?, previous?, distribution? }; ms, CLS as a score), series (rows of date with each metric's p75 under its id) and pages ({ id, url, loads, vitals }).

Examples

Device switch:

import { WebVitalsPage, type IntegrationService, type WebVitalsDevice } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Split({ service }: { service: IntegrationService }) {
  const [device, setDevice] = useState<WebVitalsDevice>("mobile");
  return <WebVitalsPage service={service} device={device} onDeviceChange={setDevice} period={28} onPeriodChange={() => {}} onConnect={async () => {}} onDisconnect={async () => {}} />;
}

Accessibility

The page has one h1; tabs follow the WAI-ARIA tabs pattern (arrow keys move, the panel is labelled by its tab). Charts and tables carry text alternatives. Disconnect asks for confirmation. Localise every string you override through labels. The verdict is text with an icon, not colour alone.

RTL & i18n

  • The whole page mirrors; charts run right to left.
  • URLs, paths and identifiers stay left-to-right inside Arabic text.
  • Every figure uses the active locale with Western digits and is bidi-isolated (Num), so +12.4% keeps its order in Arabic.
  • Metric ids (LCP, INP, CLS) and page URLs stay left-to-right.

Styling & tokens

  • Colours come from tokens (--primary, --nq-success, --nq-warning, --nq-danger, --nq-tag-*); never pass raw hex.
  • Composed from tokens only; extend with className.

Do / Don't

  • Do pass the 75th percentile.
  • Do send CLS as a plain score and the others in milliseconds.
  • Don't call a page good when one Core Web Vital is not.

Lab

https://docs.nasaqui.com/?path=/docs/components-monitoring-pages-web-vitals--docs

On this page