Bar chart
A plain SVG bar chart with a labelled axis, hairline grid and a text alternative.
npx shadcn@latest add https://ui.goodtabs.co/r/bar-chart.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/bar-chart
Foreground bars with one brand mark for the latest week.
Twelve months
A custom tick format and a wider chart.
Usage
import { BarChart } from "@/components/ui/bar-chart"
<BarChart title="Installs by week" axisLabel="Installs" data={data} highlight={4} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| data | { label: string; value: number }[] | One bar per entry, in order. | |
| title | string | Names the chart, shown as caption and SVG title. | |
| axisLabel | string | What the vertical axis measures. | |
| highlight | number | Index of the one bar drawn in the brand colour. | |
| format | (value: number) => string | Formats ticks and the text alternative. |
Source
bar-chart/bar-chart.tsx
import * as React from "react";
import { cn } from "@/lib/utils";
export type BarDatum = { label: string; value: number };
type BarChartProps = Omit<React.ComponentProps<"figure">, "children" | "title"> & {
data: BarDatum[];
/** Names the chart. Also the SVG title. */
title: string;
/** Label for the vertical axis, for example "Installs". */
axisLabel: string;
/** Index of the one mark drawn in the brand colour. */
highlight?: number;
/** Formats axis ticks and bar values. Defaults to en-US grouping. */
format?: (value: number) => string;
};
const W = 320;
const H = 180;
const M = { top: 12, right: 8, bottom: 24, left: 36 };
/** Pick a step from 1, 2, 5 times a power of ten so that max / step is at most 4. */
function niceStep(max: number) {
const raw = max / 4;
const pow = 10 ** Math.floor(Math.log10(raw));
return ([1, 2, 5, 10].find((m) => m * pow >= raw) ?? 10) * pow;
}
const group = (n: number) => new Intl.NumberFormat("en-US").format(n);
/**
* A vertical bar chart in plain SVG. Foreground tones with hairline gridlines,
* one optional brand mark. The SVG has a title and a description that lists every value.
*/
export function BarChart({ data, title, axisLabel, highlight, format = group, className, ...props }: BarChartProps) {
const id = React.useId();
const step = niceStep(Math.max(...data.map((d) => d.value), 1));
const top = Math.ceil(Math.max(...data.map((d) => d.value), 1) / step) * step;
const ticks = Array.from({ length: top / step + 1 }, (_, i) => i * step);
const plotW = W - M.left - M.right;
const plotH = H - M.top - M.bottom;
const y = (v: number) => M.top + plotH - (v / top) * plotH;
const slot = plotW / data.length;
const bar = Math.min(slot * 0.6, 28);
const summary = `${axisLabel}: ${data.map((d) => `${d.label} ${format(d.value)}`).join(", ")}.`;
return (
<figure data-slot="bar-chart" className={cn("m-0 w-full", className)} {...props}>
<svg
viewBox={`0 0 ${W} ${H}`}
role="img"
aria-labelledby={`${id}-t ${id}-d`}
className="block h-auto w-full text-foreground"
>
<title id={`${id}-t`}>{title}</title>
<desc id={`${id}-d`}>{summary}</desc>
<g aria-hidden="true">
{ticks.map((t) => (
<g key={t}>
<line
x1={M.left}
x2={W - M.right}
y1={y(t)}
y2={y(t)}
stroke="currentColor"
strokeOpacity={t === 0 ? 0.3 : 0.1}
/>
<text
x={M.left - 6}
y={y(t)}
dy="0.32em"
textAnchor="end"
className="fill-muted-foreground font-mono text-[9px]"
>
{format(t)}
</text>
</g>
))}
{data.map((d, i) => {
const cx = M.left + slot * i + slot / 2;
const h = (d.value / top) * plotH;
return (
<g key={d.label}>
<rect
x={cx - bar / 2}
y={y(d.value)}
width={bar}
height={h}
rx={2}
fill={i === highlight ? "var(--brand)" : "currentColor"}
fillOpacity={i === highlight ? 1 : 0.55}
/>
<text x={cx} y={H - 8} textAnchor="middle" className="fill-muted-foreground font-mono text-[9px]">
{d.label}
</text>
</g>
);
})}
</g>
</svg>
<figcaption className="mt-1 flex items-center justify-between text-xs text-muted-foreground">
<span>{title}</span>
<span className="font-mono">{axisLabel}</span>
</figcaption>
</figure>
);
}
For AI agents
Hand your agent this page, the markdown at /components/bar-chart/prompt.md, or the Copy AI prompt button above.
When to use
- Comparing a handful of values across categories or periods
- A dashboard card that needs a real axis
When not to use
- Many series or interactive tooltips: use a chart library
- A single number: use a stat card
Agent notes
- Plain SVG, server rendered, no chart library
- The SVG has role img, a title and a description that lists every label and value
- Bars start at zero. Values must be zero or above
- Keep the label short; at twelve bars the label is about 20px wide
- Brand is used for at most one bar, through highlight
The maths
- The axis step is the smallest of 1, 2, 5 or 10 times a power of ten that is at least max / 4, so there are at most four gridline intervals.
- The top is ceil(max / step) × step, and a bar's height is value / top × plot height.
- Bar width is min(0.6 × slot, 28) where slot is plot width / bar count.