KPI stat

One number with its change in words, an optional target meter and sparkline. Plain SVG: no chart engine.

Install

It needs no chart engine: the whole install is a few small files of React and SVG.

Plain SVG (no chart engine)

npx shadcn@latest add https://beautifulcharts.dev/r/instrument-kpi-stat.json

import { KpiStat } from "@/components/beautiful-charts/themes/instrument/kpi-stat";

Example

A complete client module. Paste it into your app after installing the component.

"use client";

import { KpiStat } from "@/components/beautiful-charts/themes/instrument/kpi-stat";

// A change from a zero base is shown as an amount, never as an infinite percentage.
export function ActiveTeams() {
  return (
    <KpiStat
      title="Active teams"
      value={74}
      previous={38}
      previousLabel="last week"
      target={80}
      trend={[38, 42, 40, 47, 51, null, 55, 61, 63, 64, 69, 74]}
      unit="teams with a weekly session"
    />
  );
}

KPI stat props

Generated from KpiStatProps in the installed source, so this table and your editor's hints always agree. * marks a required prop.

KPI stat props
PropTypeDescription
value*number | nullThe number. null or undefined shows the empty state.
previousnumber | nullThe comparison value, e.g. last period.
previousLabelstringWhat the comparison is, e.g. "last month".
targetnumber | nullA goal. Adds a meter showing progress toward it.
trend(number | null)[]Recent values, oldest first, for the sparkline. Missing values break the line.
polarity"up" | "down""up" when a rise is good (default), "down" for costs, churn, latency.
valueFormatter(value: number) => stringFormats the number, change and target.
motionMotionPreference"full" (default), "subtle" or "off". Reduced-motion settings always win.
eyebrowReactNodeSmall label above the title.
titleReactNodeCard title. A string also names the stat for screen readers.
descriptionReactNodeA line under the title.
unitReactNodeSmall unit or context under the number, e.g. "active teams".
loadingbooleanShow the loading state.
errorstring | nullAn error message; shows the error state.
densityDensity"comfortable" (default) or "compact".
classNamestringClasses on the card.
palette"theme" | "shadcn""theme" (default) or "shadcn" to use your --chart-N colours.
trendHeightnumberSparkline height in px.

Parts and styling hooks

Each part carries a data-slot attribute. Theme rules inside the chart are one attribute selector deep, so a selector such as .my-chart [data-slot="chart-title"] overrides them. These are the slots in the files this chart installs; parts a chart doesn't use are simply absent from its markup.

chartchart-bodychart-descriptionchart-eyebrowchart-footerchart-headerchart-headingchart-legendchart-legend-itemchart-legend-labelchart-legend-swatchchart-legend-valuechart-livechart-mainchart-plotchart-readoutchart-statchart-stat-notechart-stat-valuechart-statechart-state-boxchart-state-messagechart-statschart-tablechart-table-scrollchart-takeawaychart-titlechart-tooltipkpikpi-contextkpi-deltakpi-emptykpi-fillkpi-overkpi-rowkpi-targetkpi-target-labelkpi-trackkpi-trendodometerodometer-digitodometer-glyphsodometer-reelodometer-signpartial-notereadout-deltareadout-unitreadout-valuesr-only

Props every chart shares

Header, states, legend, tooltip, formatting, motion, sound and controlled interaction state. They work the same on every Instrument chart and both engines. The guide explains colours, controlled state and accessibility.

Show all 2 shared props
Props every chart shares
PropTypeDescription
localestringBCP 47 locale. Defaults to "en-US" so server and client render the same text.
timeZonestringIANA time zone for dates. Defaults to "UTC" so server and client agree.