Chart story block
BlockA number told as a story: the chart draws in once, then each beat moves a marker to its moment and explains it. Readers step through; nothing advances on its own.
pnpm dlx shadcn@latest add https://caliperui.site/r/chart-story-block.jsonGrowth
From a spreadsheet to 2,600 teams
1 of 4 · Q4 2023, 310
When to use #
Use it for
- Growth stories, customer results, an annual report section.
- Three to five moments that explain one trend.
Skip it when
- Dashboards; use the line chart.
- A single statistic; use the stats band.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/chart-story-block.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.
import { ChartStoryBlock } from "@/components/ui/chart-story-block" const teams = [120, 180, 260, 310, 420, 540, 610, 590, 720, 980, 1240, 1380, 1520, 1880, 2310, 2640].map((value, i) => ({ label: `Q${(i % 4) + 1} ${2023 + Math.floor(i / 4)}`, value })); export function ChartStoryBlockDemo() { return ( <ChartStoryBlock eyebrow="Growth" title="From a spreadsheet to 2,600 teams" chartLabel="Teams closing their books with Halden, 2023 to 2026" points={teams} format={(v) => v.toLocaleString("en-US")} beats={[ { at: 3, title: "Bank feeds launch", body: "Matching saved early customers a day a month, and they told their accountants." }, { at: 7, title: "A flat quarter", body: "We paused sign-ups to rebuild matching for larger ledgers. Teams stopped churning." }, { at: 9, title: "Accountants join", body: "Firms began moving every client over at once; growth came in batches of twenty." }, { at: 15, title: "Today", body: "2,640 teams close with Halden, and half of them finish before the 3rd." }, ]} /> ); }
API reference #
ChartStoryBlock
component
Accessibility #
It exposes img semantics, with aria-label, aria-current, aria-hidden and aria-live kept in step with state. Changes that matter are announced through a live region.
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.