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-chartsWith pnpm or yarn:
pnpm add ztd-chartsyarn 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.
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>
);
}Pie chart
- Mobile
- Desktop
- Tablet
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: #006DA8Change with a CSS variable
Same variable, another chart
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 variable | Dark | Light | Use |
|---|---|---|---|
| bg--ztd-bg | #14171B | #FFFFFF | Page background |
| panel--ztd-panel | #14171B | #FFFFFF | Card background |
| ink--ztd-ink | #F1F3F5 | #14171B | Text, second emphasis |
| muted--ztd-muted | #A3ABB5 | #5D6570 | Secondary text (AA, 4.5:1 or more) |
| dim--ztd-dim | #5B636D | #B3B9C2 | Axes and guide lines, not text |
| accent--ztd-accent | #D6FD70 | #0B8FD8 | The one highlight |
| accentText--ztd-accent-text | #D6FD70 | #006DA8 | Accent used as text |
| onAccent--ztd-on-accent | #14171B | #14171B | Text on the accent |
| onInk--ztd-on-ink | #14171B | #FFFFFF | Text on ink shapes |
| line--ztd-line | #2A2F36 | #E6E9ED | Borders, gridlines |
| line2--ztd-line2 | #22272D | #F0F2F5 | Secondary lines, tracks |
| neutral--ztd-neutral | #3A414A | #D5DAE0 | Fill for the other items |
| neutralStrong--ztd-neutral-strong | #5F6772 | #9AA3AE | Stronger fill |
| onNeutralStrong--ztd-on-neutral-strong | #F1F3F5 | #14171B | Text on neutralStrong |
| neutralSoft--ztd-neutral-soft | #262B31 | #EEF1F4 | Lighter fill, tracks |
| lift--ztd-lift | #1D2126 | #F4F6F8 | Hover and focus background |
| tint--ztd-tint | rgba(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:
: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.
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:
| Call | en-US | ko-KR |
|---|---|---|
| formatCompact(12300) | 12.3K | 1.2만 |
| formatCompact(340000000) | 340M | 3.4억 |
| formatCurrency(5000) | $5,000.00 | ₩5,000 |
| formatDate("2025-03-15") | Mar 15 | 3월 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")andchartsIn("ko")give Korean names, descriptions and examples, and every sample has a Korean twin such asdonutSampleKo. validateSpec(spec, { lang: "ko" })returns Korean messages.- CLI: add
--locale koanywhere on the command line. - MCP: pass
locale: "ko"torender_chart, or setoptions.localein 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.
<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 }.
{
"$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.
| Option | Description |
|---|---|
heightnumber | Height in px. Without it, each chart picks a default that fits its width |
widthnumber | Fixed 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 |
unitstring | Unit 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 true | Entry and update animation |
durationnumber | Animation length (ms) |
easing"spring" | "easeOut" | "easeInOut" | "linear"Default "spring" | Motion curve |
staggernumber | Delay between items as they enter (ms) |
reducedMotionanyDefault "auto" | "auto" follows the OS setting. true always turns motion off |
animateOnViewbooleanDefault true | Play when the chart scrolls into view |
showLabelsbooleanDefault true | Labels drawn on the chart (values and names) |
tooltiptooltipDefault true | Show 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 |
syncIdstring | Charts with the same syncId show the hovered x together (line, area, bar, candlestick). <ChartGrid sync> sets one for every chart inside it |
ariaLabelstring | Name for screen readers |
descriptionstring | Description 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.
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 messagesMCP server
The stdio MCP server provides five tools:
list_chartsget_chart_schemavalidate_specrender_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:
claude mcp add ztd-charts -- npx -y ztd-charts-mcpClaude Desktop, Cursor and other clients take the same JSON in their config file:
- Claude Desktop:
claude_desktop_config.json - Cursor:
.cursor/mcp.jsonin a project, or~/.cursor/mcp.jsonfor every project
{
"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.
| Import | Brotli | Brotli |
|---|---|---|
| RadialBarChart | 11.8 KB | |
| GaugeChart | 12.0 KB | |
| DumbbellChart | 12.3 KB | |
| FunnelChart | 12.3 KB | |
| WaffleChart | 12.5 KB | |
| SegmentedBar | 12.5 KB | |
| ChordChart | 12.5 KB | |
| BulletChart | 12.6 KB | |
| SlopeChart | 12.8 KB | |
| Treemap | 12.8 KB | |
| RidgelineChart | 13.0 KB | |
| MarimekkoChart | 13.1 KB | |
| LollipopChart | 13.1 KB | |
| SunburstChart | 13.2 KB | |
| WaterfallChart | 13.4 KB | |
| SankeyChart | 13.5 KB | |
| ParallelCoordinates | 13.6 KB | |
| LiveLineChart | 13.6 KB | |
| ParetoChart | 13.7 KB | |
| RadarChart | 13.7 KB | |
| PieChart | 13.9 KB | |
| DonutChart | 13.9 KB | |
| BeeswarmChart | 14.1 KB | |
| Heatmap | 14.2 KB | |
| BoxPlot | 14.4 KB | |
| BumpChart | 14.5 KB | |
| Sparkline | 14.5 KB | |
| CalendarHeatmap | 15.0 KB | |
| StatCard | 15.1 KB | |
| BubbleChart | 16.1 KB | |
| StreamGraph | 16.3 KB | |
| HistogramChart | 18.3 KB | |
| SkylineChart | 18.7 KB | |
| ScatterChart | 18.9 KB | |
| CandlestickChart | 20.1 KB | |
| BarChart | 20.4 KB | |
| LineChart | 26.0 KB | |
| AreaChart | 26.0 KB |