Docs

Get started

Install the package and draw your first chart.

Install

Requires React 18 or later. There are no other runtime dependencies.

npm i ztd-charts

With pnpm or yarn:

  • pnpm add ztd-charts
  • yarn add ztd-charts

Pretendard is the recommended font; without it, the system font is used. In the Next.js App Router, charts already carry their own "use client" boundary.

Quick start

Pass an array of objects and name its fields in props. Pick the one item that should stand out with highlight. The card adds a number, title, legend, one-line takeaway and level badge.

DeviceShare.tsx
import { ChartCard, PieChart } from "ztd-charts";

const data = [
  { device: "Mobile", sales: 72 },
  { device: "Desktop", sales: 23 },
  { device: "Tablet", sales: 5 },
];

export function DeviceShare() {
  return (
    <ChartCard
      number="01"
      title="Pie chart"
      subtitle="Revenue share by device"
      takeaway="Keep it to 2 to 4 items"
      level="basic"
      replay
      legend={[
        { label: "Mobile", color: "accent" },
        { label: "Desktop", color: "ink" },
        { label: "Tablet", color: "neutral" },
      ]}
    >
      <PieChart data={data} label="device" value="sales"
        highlight="Mobile" />
    </ChartCard>
  );
}
01

Pie chart

Revenue share by device
  • Mobile
  • Desktop
  • Tablet
Pie chartPie chart. 3 items, total 100. Mobile 72%, Desktop 23%, Tablet 5%.72%23%5%
Pie chart data
Mobile72% · 72
Desktop23% · 23
Tablet5% · 5
Keep it to 2 to 4 itemsBasic

Theme

Two themes ship with the same colors as the landing page. Light has white cards and a sky accent (#0B8FD8); dark has ink cards (#14171B) and a lime accent (#D6FD70). Without a ThemeProvider, charts draw in dark; this site starts in light. Cards have an 18px radius.

Every color is a --ztd-* CSS variable, so one line of CSS can change it. Pick a color below and the charts change without React re-rendering. The demo also sets --ztd-accent-text, mixed toward ink until it reaches 4.5:1 on the card.

--ztd-accent: #0B8FD8; --ztd-accent-text: #006DA8

Change with a CSS variable

--ztd-accent
Change with a CSS variableDonut chart. 4 items, total 1,240. Purchase 58%, Sign-up 21%, Inquiry 13%, Call 8%.1,240ConversionsPurchase58%Sign-up21%Inquiry13%Call8%
Change with a CSS variable data
Purchase58% · 719
Sign-up21% · 260
Inquiry13% · 161
Call8% · 100
No re-renderBasic

Same variable, another chart

Monthly active users
Same variable, another chartLine chart. 3 series (Our app, Rival A, Rival B), x from Jan to Dec, 12 points.Our appRival ARival B
Same variable, another chart data
JanOur app 42, Rival A 56.8, Rival B 38
FebOur app 46.8, Rival A 57.3, Rival B 38.8
MarOur app 50.3, Rival A 57.1, Rival B 37.6
AprOur app 52.2, Rival A 56.8, Rival B 35.2
MayOur app 53.4, Rival A 56.9, Rival B 34.3
JunOur app 55.3, Rival A 57.8, Rival B 35.3
JulOur app 58.9, Rival A 59.5, Rival B 35.9
AugOur app 63.7, Rival A 61.6, Rival B 34.3
SepOur app 68.5, Rival A 63.6, Rival B 32
OctOur app 72, Rival A 64.9, Rival B 31.5
NovOur app 73.9, Rival A 65.4, Rival B 32.6
DecOur app 75.1, Rival A 65.2, Rival B 32.9
One line recolors every chartBasic
theme.tsx
import { ThemeProvider } from "ztd-charts";

// 1) By name: "light" or "dark" (the default)
<ThemeProvider theme="light" paint>…</ThemeProvider>

// 2) Only the tokens you change
<ThemeProvider theme={{ accent: "#F7465C", accentText: "#C42B40" }}>…</ThemeProvider>

// 3) CSS variables only (no React re-render)
.my-report { --ztd-accent: #00A3FF; --ztd-panel: #0B1220; }

// 4) One chart only
<BarChart theme={{ accent: "#18A058" }} … />
Token · CSS variableDarkLightUse
bg--ztd-bg#14171B#FFFFFFPage background
panel--ztd-panel#14171B#FFFFFFCard background
ink--ztd-ink#F1F3F5#14171BText, second emphasis
muted--ztd-muted#A3ABB5#5D6570Secondary text (AA, 4.5:1 or more)
dim--ztd-dim#5B636D#B3B9C2Axes and guide lines, not text
accent--ztd-accent#D6FD70#0B8FD8The one highlight
accentText--ztd-accent-text#D6FD70#006DA8Accent used as text
onAccent--ztd-on-accent#14171B#14171BText on the accent
onInk--ztd-on-ink#14171B#FFFFFFText on ink shapes
line--ztd-line#2A2F36#E6E9EDBorders, gridlines
line2--ztd-line2#22272D#F0F2F5Secondary lines, tracks
neutral--ztd-neutral#3A414A#D5DAE0Fill for the other items
neutralStrong--ztd-neutral-strong#5F6772#9AA3AEStronger fill
onNeutralStrong--ztd-on-neutral-strong#F1F3F5#14171BText on neutralStrong
neutralSoft--ztd-neutral-soft#262B31#EEF1F4Lighter fill, tracks
lift--ztd-lift#1D2126#F4F6F8Hover and focus background
tint--ztd-tintrgba(214, 253, 112, 0.14)rgba(11, 143, 216, 0.12)Intermediate badge background

On the card, dim is 2.95:1 in dark and 1.97:1 in light, below WCAG AA (4.5:1) for small text. So text uses muted, and dim is only for axes and lines. The light accent (#0B8FD8) is tuned for shapes on white (3:1), so text uses the darker accentText (#006DA8). The dark lime is bright enough as text on ink, so its two values match.

To use a theme as plain CSS, themeToCss(lightTheme) gives you its variables:

light.css
:root {
  --ztd-bg: #FFFFFF;
  --ztd-panel: #FFFFFF;
  --ztd-ink: #14171B;
  --ztd-muted: #5D6570;
  --ztd-dim: #B3B9C2;
  --ztd-accent: #0B8FD8;
  --ztd-accent-text: #006DA8;
  --ztd-on-accent: #14171B;
  --ztd-on-ink: #FFFFFF;
  --ztd-line: #E6E9ED;
  --ztd-line2: #F0F2F5;
  --ztd-neutral: #D5DAE0;
  --ztd-neutral-strong: #9AA3AE;
  --ztd-on-neutral-strong: #14171B;
  --ztd-neutral-soft: #EEF1F4;
  --ztd-lift: #F4F6F8;
  --ztd-tint: rgba(11, 143, 216, 0.12);
  --ztd-font: "Pretendard Variable", Pretendard, "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", system-ui, -apple-system, sans-serif;
  --ztd-mono: "Roboto Mono", "Share Tech Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --ztd-radius: 18px;
  --ztd-radius-code: 14px;
  --ztd-radius-mark: 4px;
}

Language

Charts follow a locale. It sets number and date formats and the text a chart writes itself: tooltips, screen reader text, buttons and badges. English (en-US) is the default; ko or any ko-* locale switches to Korean.

locale.tsx
import { BarChart, ThemeProvider } from "ztd-charts";

// Every chart and card inside follows this locale
<ThemeProvider locale="ko-KR">…</ThemeProvider>

// One chart only. The prop wins over the provider
<BarChart data={rows} x="month" y="visits" locale="en-US" />

The same values in both locales, computed on this page:

Callen-USko-KR
formatCompact(12300)12.3K1.2만
formatCompact(340000000)340M3.4억
formatCurrency(5000)$5,000.00₩5,000
formatDate("2025-03-15")Mar 153월 15일
  • Built-in text follows the locale too: "Replay animation", "Try again" and "Couldn't load data" become "애니메이션 다시 재생", "다시 시도" and "데이터를 불러오지 못했어요".
  • Registry text is English. localizeDef(def, "ko") and chartsIn("ko") give Korean names, descriptions and examples, and every sample has a Korean twin such as donutSampleKo.
  • validateSpec(spec, { lang: "ko" }) returns Korean messages.
  • CLI: add --locale ko anywhere on the command line.
  • MCP: pass locale: "ko" to render_chart, or set options.locale in the spec.

Animation

Every chart enters when it scrolls into view. The spring is critically damped, so it never overshoots, and when data changes mid-motion the current velocity carries over. All charts share one requestAnimationFrame loop. Charts follow the OS reduced motion setting, or you can set reducedMotion yourself.

motion.tsx
<LineChart
  data={rows} x="month" y="mau" series="app"
  animate              // default true
  easing="spring"      // "spring" | "easeOut" | "easeInOut" | "linear"
  duration={900}       // ms
  stagger={120}        // delay between items
  animateOnView        // start when it scrolls into view (default true)
  replayKey={n}        // start over when this changes
  reducedMotion="auto" // follow the OS reduced motion setting
/>
// When data changes, the chart moves on from where it is (velocity carries over mid-change)

Accessibility

  • Each chart is an SVG with role="img", a title and a description built from the data. A visually hidden data table is included too.
  • Tab in, move between items with the arrows, Home and End, and click with Enter or Space. The selected item is announced through aria-live.
  • Text on the accent and on ink shapes has its own contrast-checked tokens. Heatmap cell labels pick their color by computing contrast against the cell.
  • With reduced motion on, charts draw their final state right away.

JSON spec

A serializable { type, data, encoding, options, card } spec, with no functions, renders through <Chart spec />. The public JSON Schema (draft 2020-12) is at ztd-charts/schema.json. data takes an array of rows, { csv } or { url, select }.

funnel.spec.json
{
  "$schema": "https://unpkg.com/ztd-charts/schema/chart-spec.schema.json",
  "type": "funnel",
  "data": [
    { "stage": "Visit", "users": 10000 },
    { "stage": "Product view", "users": 6200 },
    { "stage": "Cart", "users": 1900 },
    { "stage": "Purchase", "users": 760 }
  ],
  "encoding": { "label": "stage", "value": "users" },
  "options": { "unit": " users" },
  "card": { "number": "03", "title": "Funnel chart", "takeaway": "Drop-off by stage", "level": "basic" }
}

Common options

Every chart takes these props. In a spec they go under options.

OptionDescription
heightnumberHeight in px. Without it, each chart picks a default that fits its width
widthnumberFixed width in px. Without it, the chart fits its parent
highlightnumber | string | (number|string)[] | "max" …Item to highlight. An index, a name, an array, or "max" | "min" | "first" | "last" | "none"
format"number" | "percent" | "compact" | …Value format. "number" | "integer" | "percent" (values are already percents: 42 → "42%") | "ratio" (0.42 → "42%") | "compact" | "currency" (KRW for Korean locales, otherwise USD; for another currency pass Intl options with currency) | "decimal" or Intl.NumberFormat options
unitstringUnit appended to values. For example "%" or "ms"
localestringDefault "en-US"Locale for numbers, dates and the text a chart writes itself (tooltips, screen reader sentences). "ko" or "ko-*" switches to Korean. Falls back to the ThemeProvider locale
animatebooleanDefault trueEntry and update animation
durationnumberAnimation length (ms)
easing"spring" | "easeOut" | "easeInOut" | "linear"Default "spring"Motion curve
staggernumberDelay between items as they enter (ms)
reducedMotionanyDefault "auto""auto" follows the OS setting. true always turns motion off
animateOnViewbooleanDefault truePlay when the chart scrolls into view
showLabelsbooleanDefault trueLabels drawn on the chart (values and names)
tooltiptooltipDefault trueShow tooltips. { template: "{label} · {value} · owner {owner}" } writes them: {label}, {value}, {series} or any other column of the row. \n starts a new line, and a line with an empty value is left out
syncIdstringCharts with the same syncId show the hovered x together (line, area, bar, candlestick). <ChartGrid sync> sets one for every chart inside it
ariaLabelstringName for screen readers
descriptionstringDescription for screen readers. Built from the data when omitted

CLI

ztd-charts lists charts, prints schemas, validates specs, renders SVG, PNG or HTML, writes starter files and suggests charts from your data. Run it with npx ztd-charts-cli, or install that package to get the ztd-charts command (npx ztd-charts alone fetches the chart library, which has no command). PNGs use Pretendard when it's installed. Add --locale ko anywhere on the command line for Korean messages, chart text and number formats.

Terminal
npx ztd-charts-cli list                      # every chart
npx ztd-charts-cli info sankey               # channels, options, example, React code
npx ztd-charts-cli example pie --out spec.json   # a spec to start from
npx ztd-charts-cli validate spec.json        # exits 1 on errors, with "Did you mean …?"
npx ztd-charts-cli render spec.json --out chart.png --theme light --scale 2
npx ztd-charts-cli render spec.json --out chart.html --interactive
npx ztd-charts-cli schema line --out line.schema.json
npx ztd-charts-cli init my-app --type funnel
npx ztd-charts-cli suggest data.csv --out-dir charts
npx ztd-charts-cli validate spec.json --locale ko   # Korean messages

MCP server

The stdio MCP server provides five tools:

  • list_charts
  • get_chart_schema
  • validate_spec
  • render_chart (a PNG image, SVG and React code)
  • suggest_chart

Pass locale: "ko" to render_chart for Korean chart text and number formats.

Data URLs on a private network or localhost are refused. To load from a local dev server, add ZTD_CHARTS_ALLOW_PRIVATE_URLS=1 to the server's environment: -e in claude mcp add, or the env field in the JSON.

In Claude Code, add it from a terminal:

Terminal
claude mcp add ztd-charts -- npx -y ztd-charts-mcp

Claude Desktop, Cursor and other clients take the same JSON in their config file:

  • Claude Desktop: claude_desktop_config.json
  • Cursor: .cursor/mcp.json in a project, or ~/.cursor/mcp.json for every project
JSON
{
  "mcpServers": {
    "ztd-charts": {
      "command": "npx",
      "args": ["-y", "ztd-charts-mcp"]
    }
  }
}

AI skill

skills/ztd-charts/SKILL.md teaches an agent to pick a chart, write a spec and connect real data. Copy the skills/ztd-charts folder from the GitHub repository into .claude/skills/ in your project; Claude Code and Cursor both read it there. llms.txt at the site root is the summary.

Bundle size

The package is ESM with sideEffects: false, so only the charts you import ship. Below are Brotli sizes measured with size-limit, React not included. The shared core (theme, motion, frame) ships once.

ImportBrotliBrotli
RadialBarChart11.8 KB
GaugeChart12.0 KB
DumbbellChart12.3 KB
FunnelChart12.3 KB
WaffleChart12.5 KB
SegmentedBar12.5 KB
ChordChart12.5 KB
BulletChart12.6 KB
SlopeChart12.8 KB
Treemap12.8 KB
RidgelineChart13.0 KB
MarimekkoChart13.1 KB
LollipopChart13.1 KB
SunburstChart13.2 KB
WaterfallChart13.4 KB
SankeyChart13.5 KB
ParallelCoordinates13.6 KB
LiveLineChart13.6 KB
ParetoChart13.7 KB
RadarChart13.7 KB
PieChart13.9 KB
DonutChart13.9 KB
BeeswarmChart14.1 KB
Heatmap14.2 KB
BoxPlot14.4 KB
BumpChart14.5 KB
Sparkline14.5 KB
CalendarHeatmap15.0 KB
StatCard15.1 KB
BubbleChart16.1 KB
StreamGraph16.3 KB
HistogramChart18.3 KB
SkylineChart18.7 KB
ScatterChart18.9 KB
CandlestickChart20.1 KB
BarChart20.4 KB
LineChart26.0 KB
AreaChart26.0 KB