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.
| month | Projects published | Proposals accepted |
|---|---|---|
| January | 24 | 18 |
| February | 31 | 22 |
| March | 28 | 25 |
| April | 35 | 30 |
| May | 42 | 36 |
| June | 38 | 33 |
Install
bun add @voila.dev/ui@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.
| month | Projects published |
|---|---|
| January | 24 |
| February | 31 |
| March | 28 |
| April | 35 |
| May | 42 |
| June | 38 |
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
- Gallery — every chart type on one screen.
- Chart.Root — the frame: data, config, scales, interaction.
- Chart.Bars — vertical or horizontal bars.
- Stacked bars — series stacked in one column.
- Chart.Area — filled series, optional gradient.
- Chart.Line — lines, curves and steps.
- Chart.Points — scatter and bubble marks.
- Chart.Pie — parts of a whole.
- Chart.Donut — the pie with a hole for a number.
- Chart.Radar — one shape across several axes.
- Chart.RadialBar — concentric progress rings.
- Chart.Grid — background rules.
- Chart.XAxis — bottom ticks and labels.
- Chart.YAxis — left ticks and labels.
- Chart.Cursor — the hover band or line.
- Chart.Tooltip — the readout for the active datum.
- Chart.Legend — name the series.
- Chart.LabelList — values written on the marks.
- Chart.ReferenceLine — a target or threshold.
- Chart.Skeleton — the loading state.
- Chart.Empty — the no-data state.