Dumbbell chart
FreeA before and an after per row joined by a bar. The after dot travels out from the before value, and sorting by change moves rows to their new places.
pnpm dlx shadcn@latest add https://caliperui.site/r/dumbbell-chart.jsonInvite a teammate+15%
Import data+7%
Connect bank−3%
First rule+15%
Share a report+3%
| Row | August | September | Change |
|---|---|---|---|
| Invite a teammate | 31 | 46 | 15 |
| Import data | 54 | 61 | 7 |
| Connect bank | 42 | 39 | -3 |
| First rule | 22 | 37 | 15 |
| Share a report | 18 | 21 | 3 |
When to use #
Use it for
- Scores before and after a launch, prices last year and this.
- Many rows, each with one change.
Skip it when
- More than two moments; use the slope chart or a line chart.
- A single row; write the two numbers.
Installation #
terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/dumbbell-chart.json
Adds its 7 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.
example.tsx
import { DumbbellChart } from "@/components/ui/dumbbell-chart" export function DumbbellChartDemo() { return ( <DumbbellChart label="Activation by onboarding step, before and after the new checklist" beforeLabel="August" afterLabel="September" unit="%" className="w-full" rows={[ { id: "invite", label: "Invite a teammate", before: 31, after: 46 }, { id: "import", label: "Import data", before: 54, after: 61 }, { id: "connect", label: "Connect bank", before: 42, after: 39 }, { id: "rule", label: "First rule", before: 22, after: 37 }, { id: "report", label: "Share a report", before: 18, after: 21 }, ]} /> ); }
API reference #
DumbbellChart
component
PropTypeDefault
label *string—
rows *readonly DumbbellRow[]—
beforeLabelstring"Before"
afterLabelstring"After"
unitstring""
formatValue((value: number) => string)numberText
higherIsBetterbooleantrue
loadingboolean—
errorstring—
classNamestring—
Accessibility #
It exposes list and listitem semantics, with aria-label, aria-busy and aria-hidden 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.