Skip to content

Trends & comparisons

Numbers over time and side by side: KPIs, metric charts, running totals, retention, percentiles, curves and head-to-heads.

Installation & usage

npx @youj/cli add app-kpi-cards

Also installs alert, button, card, empty, icon-info, segmented-control, stat-card, tooltip, use-announcer, use-controllable-state, youj-core, youj-presets, youj-ui-core.

The CLI copies the block to blocks/app/kpi-cards.tsx with every component it uses, so it’s yours to edit.

Every prop is optional and falls back to the demo, so it renders as you see it until you pass your own. Give it your data, rewrite or translate its strings through copy, and connect its actions:

kpiCards-view.tsx
'use client'

import { KpiCards, type KpiCardsProps } from '@/blocks/app/kpi-cards'

export function KpiCardsView({ kpis, dates, granularity, ranges }: Pick<KpiCardsProps, 'kpis' | 'dates' | 'granularity' | 'ranges'>) {
  return (
    <KpiCards
      kpis={kpis}
      dates={dates}
      granularity={granularity}
      ranges={ranges}
      copy={{ title: 'Overview' }}
      onRangeChange={(range) => {
        // your code
      }}
      onRetry={() => {
        // your code
      }}
    />
  )
}

Reference

Props19 props

Props

KpiCards props
PropType
themeThemeInput

Wraps the block in this theme. Default: the theme around it.

–
embeddedboolean

Inside another app: a section named by an h2, without the page around it.

default true

true
fillboolean

Take the whole width it's given instead of stopping at its natural width (1024px at 100% scale).

default false

false
headingLevel1 | 2 | 3

The title's level. Default: 2 when embedded, else 1.

–
copyPartial<KpiCardsCopy>

Any of the block's strings, to rewrite or translate.

–
localestring

Formats every figure and date (BCP 47). Default: copy.locale, else the app's locale (YoujProvider), else en-US.

–
kpisKpi[]

The tiles, in order, each with one reading per bucket. Default: the demo shop's revenue, new customers, conversion and refund rate.

–
datesstring[]

When each bucket starts (ISO dates, read as UTC), shared by every KPI. Default: the demo's for the range.

–
granularityKpiCardsGranularity
'day''hour''week''month'

How long a bucket is, for the dates. Default: the demo's for the range, else day.

–
variantKpiCardsVariant
'trend''bars''minimal'

minimal (figures on hairlines), trend (a line each) or bars (a bar per bucket).

default 'trend'

'trend'
partialboolean

The last bucket is still filling (today): its segment dashed, read "so far". Default: true with the demo's data, else false.

–
rangesRange[]

The windows the range switch offers; fewer than two hides it.

default 7D, 30D and 90D

7D, 30D and 90D
rangestring

The id of the window shown, controlled: pair it with onRangeChange. Default: uncontrolled.

–
defaultRangestring

The window shown first, uncontrolled.

default `30d`

`30d`
onRangeChange(range: string) => void

Called with the window picked; answer with that window's kpis and dates.

–
loadingboolean

Each tile holds a skeleton while your data loads.

default false

false
errorReactNode

Shown in place of the tiles when they fail to load, with Retry when onRetry is set. Default: none.

–
onRetry() => void

Retries after an error. Default: none.

–
emptyReactNode

Replaces the message when there are no KPIs. Default: copy.empty and copy.emptyDescription.

–
Strings16 keys for copy

Strings

copy takes any of these keys; the rest keep their defaults. A key whose text holds a value (a count, a name) takes a function.

titlerangeversussoFarweekOfaboutscrubpointupdownannouncementloadingretryemptyemptyDescriptionlocale
Built from7 components
Accessibilitysliders, grids, text summaries, motion

Accessibility

Each chart has a name and a one-sentence summary, and the metric chart's numbers are also in a real table behind View data. Wherever you can scrub a chart (the KPI tiles and the metric chart), it is a slider: Left and Right step a bucket, Home and End jump to either end, Escape lets go, and each position is read out as a sentence with the date, the figures and the change. The tile and chart headers update with it.

The metric chart's legend entries are toggle buttons with aria-pressed, and hiding or soloing a series is announced. Annotations are buttons named with what happened, the date and the details. The breakdown's ring is decorative: its parts are a list of buttons named with figure, share, change and how many parts they hold, the path is a breadcrumb, and focus lands in the new level after a zoom. The retention grid is one tab stop: the arrow keys move along the triangle, Home and End move along a row, and every cell is named with its cohort, period, share and count, marked when still in progress. The goals are a radio group, so arrow keys pick the goal the burn-up shows.

The daily bars and the running total are each one tab stop, a group named for the chart and described by a sentence on the total and the extremes. Left and Right read a day, Home and End jump to either end and Escape lets go; the day is read out by a polite live region, and the callouts and end values are text.

The live card's count is announced politely every 30 seconds while it runs, and Pause stops every update (WCAG 2.2.2). Its demo timers stop while the tab is hidden or the card is off screen.

Percentile trend's strip has a visually hidden summary of its quantiles, the window switch is labelled, stats are a description list with a label, the items are a labelled section, and an item's status is added as hidden text. The new p99 is announced when the window changes.

Hour lanes draws a named grid with a row per lane (its header the name and share) and a cell per bucket named with its time and count, and "highlighted" while the Off-hours switch is on; the grid is one tab stop, the arrow keys walk hours and sources, and focus shows the same tooltip as hovering. Off-hours is a labelled switch, and the foot's figures are a description list.

Each of the quadrant's split lines has a handle that is a slider, named for the measure it splits and read with its value; the arrow keys, Page Up and Down, Home and End move it, and the new counts are announced once it stops. The bubbles are one tab stop, an arrow key goes to the nearest bubble that way, and each is named with its figures, its group and, with Movement on, the group it came from. The chips are toggle buttons named with their count.

Compare items' radar and overview are one reading: the radar names each point with the model, axis and score, and has its own hidden table, and the overview is a real table with the models as columns. Each column's picker is a labelled select ("Model 1"), the remove buttons are named with the model they remove, and a best value is marked in text for assistive tech ("best in this row"), not only by its dot. Highlight best is a switch.

Head to head groups contenders, metrics and the trade-off under labels. Each bar row carries a hidden verdict naming the winner, the quadrant is an image with a text alternative that states the trade-off, and the choose control is labelled.

Paired bars is a table: each number carries its side's name, and the bars are for the eye. Under reduced motion every value lands in place; the limit meters' bucket still refills a token at a time.

Curve window picks the series with one named choice, draws the curves as an image whose text gives the series, the number of parts, how many fell outside the window and the window itself, and lists every part with its result and where it ended in words. The failed part's callout is plain text on the page, not a hover tooltip.

Drift histogram draws the plate as an image whose text says how many holes landed, how many are older, recent and outside the tolerance, and how far the recent ones sit from nominal; the offset, the capability and the tool offset are text, and Apply announces that the offset was applied.

Coverage grid draws a named grid with a row per row (the picked one aria-selected) and every cell named with its column and depth ("Cloud: No rules"); the grid is one tab stop, the arrows move between rows and cells, and Enter picks a row. Gaps are hatched and dashed as well as colored. The suggestion is a polite live region, so picking a row or drafting is read out.

Under reduced motion every figure, line, ring and bar is drawn at its value, and the now line and hand move on each minute without gliding. Scrubbing still follows the pointer and keys, the breakdown changes level without zooming, and the live dot stops pulsing. The heatmap's days, the flow's ribbons and the quadrant's bubbles are drawn in place, and a new range or a moved split changes them at once.