Bullet chart
FreeA KPI against its target in one line: the measure fills across poor, fair and good bands toward a target tick, with the gap written out.
pnpm dlx shadcn@latest add https://caliperui.site/r/bullet-chart.json- New ARR$k$412k of $450k · Behind target (−$38k)
- Net revenue retention112% of 110% · On target (+2%)
- Logo churn1.9% of 2.5% · On target (−0.6%)
- API uptime99.93% of 99.95% · Behind target (−0.02%)
When to use #
Use it for
- Quarterly goals, SLOs, budgets.
- Several targets compared at a glance.
Skip it when
- One value with no target; use the meter.
- Progress through steps; use progress.
Installation #
terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/bullet-chart.json
Adds its 3 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above. It is built from bulletStatus and BulletChart.
example.tsx
import { BulletChart } from "@/components/ui/bullet-chart" export function BulletChartDemo() { const money = (v: number) => `$${v.toLocaleString("en-US")}k`; return ( <BulletChart label="Q3 goals" className="w-full max-w-2xl" items={[ { id: "arr", label: "New ARR", detail: "$k", value: 412, target: 450, bands: [300, 400, 520], format: money }, { id: "nrr", label: "Net revenue retention", value: 112, target: 110, bands: [95, 105, 125], format: (v) => `${v}%` }, { id: "churn", label: "Logo churn", value: 1.9, target: 2.5, bands: [2, 3, 5], lowerIsBetter: true, format: (v) => `${v}%` }, { id: "uptime", label: "API uptime", value: 99.93, target: 99.95, bands: [99.8, 99.9, 100], format: (v) => `${v}%` }, ]} /> ); }
API reference #
bulletStatus
component
PropTypeDefaultDescription
id *string—
label *string—
detailstring—e.g. "Revenue, $k".
value *number—
target *number—
bands *readonly [number, number, number]—Upper bounds of the poor, fair and good bands. The last one is the scale's maximum.
lowerIsBetterboolean—Lower is better, e.g. churn or latency.
format((value: number) => string)—
BulletChart
component
PropTypeDefault
label *string—
items *readonly BulletItem[]—
loadingboolean—
errorstring—
classNamestring—
Accessibility #
It exposes img semantics, with aria-label and aria-busy kept in step with state.
No custom key handling; Tab and the native activation keys do the work.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.