Quick start

Charts with zero charting library. SVG you can read, scales included.

Charts with zero charting library. SVG you can read, scales included.

Most chart code in a product is a wrapper around a dependency nobody on the team has opened. This package removes the dependency instead of wrapping it: the scales, ticks and geometry are a few hundred lines of arithmetic that ship as source, so the chart on your dashboard is code you can read, step through and rewrite. Own it the same way you own a button.

JanFebMarAprMayJun01020304050
Chart of Projects published, Proposals accepted over 6 points
monthProjects publishedProposals accepted
January2418
February3122
March2825
April3530
May4236
June3833

Install

bun add @voila.dev/ui
globals.css
@import "@voila.dev/ui/styles/sources.css";

Or copy the source — it's the same files.

Peers: react@19, react-dom@19, tailwindcss@4.

The 3-minute win

A line chart of monthly bookings, complete: data, config, chart.

JanFebMarAprMayJun01020304050
Chart of Projects published over 6 points
monthProjects published
January24
February31
March28
April35
May42
June38
import { Chart, type ChartConfig } from "@voila.dev/ui/chart";
 
const data = [
	{ month: "January", bookings: 18 },
	{ month: "February", bookings: 22 },
	{ month: "March", bookings: 25 },
	{ month: "April", bookings: 30 },
	{ month: "May", bookings: 36 },
	{ month: "June", bookings: 33 },
];
 
const config = {
	bookings: { label: "Bookings confirmed", color: "var(--chart-1)" },
} satisfies ChartConfig;
 
export function Bookings() {
	return (
		<Chart.Root
			config={config}
			data={data}
			x={{ key: "month" }}
			y={{ keys: ["bookings"] }}
			className="w-full"
		>
			<Chart.Grid />
			<Chart.XAxis />
			<Chart.YAxis />
			<Chart.Cursor />
			<Chart.Line />
			<Chart.Tooltip />
		</Chart.Root>
	);
}

Mental model

Chart.Root measures its box, derives the scales from your data and owns the active datum. Every other part draws into that shared frame: marks (Chart.Bars, Chart.Line, Chart.Pie, …) render the data, furniture (Chart.Grid, Chart.XAxis, Chart.Legend, …) explains it. A chart is composed the way the rest of your UI is composed, out of children. Pointer, touch and keyboard all drive the same active index, and the numbers are always available as a visually hidden data table.

Series colours default to the --chart-1--chart-5 tokens, so charts follow a rebrand like everything else.

Why no charting library

A charting dependency is a wall between you and your pixels: a renderer you cannot step into, a bundle you pay for on every page, an API your agent can only guess at. Here the whole pipeline is plain SVG driven by plain arithmetic, sitting in node_modules as readable .tsx. When a tick lands in the wrong place you read the function that placed it. That is the AI-readable argument in miniature: a model can trace the chart because there is nothing underneath to hallucinate around.

Page map