caliper uidocs

Chart story block

Block

A 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.

Installs components/ui/chart-story-block.tsxDepends on d3-shape, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/chart-story-block.json

Growth

From a spreadsheet to 2,600 teams

310Q1 2023Q4 2026

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 #

terminal
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.

example.tsx
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
PropTypeDefaultDescription
eyebrowstring—
title *string—
leadstring—
points *readonly StoryPoint[]—
beats *readonly StoryBeat[]—`at` is the index of the point the beat is about.
format((value: number) => string)(v) => new Intl.NumberFormat("en-US", { notation: "compact" }).format(v)
chartLabel *string—Spoken name of the chart, e.g. "Weekly active teams, 2023 to 2026".
classNamestring—

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.