Bar chart for React
Columns and bars with rounded data ends, a 2 px surface gap, value labels where they fit, tooltips and arrow keys.
One of 7 components in Charts
Compare categories at a glance
A bar chart compares amounts across categories, and the eye reads length more precisely than any other shape. This one draws columns and bars with rounded data ends and a small gap between stacked segments, puts value labels where they fit and moves through bars with the arrow keys.
Simple shows one value per category, the default for rankings and monthly totals. Grouped places two or three series side by side, for example plan A against plan B per month. Stacked shows the parts of each total, Horizontal turns the bars sideways for long category names such as product or country names.
Start the value axis at zero, a bar that starts at 80 lies about its length. Sort bars by value unless the categories have their own order such as months. Use the accent for the bar that matters and a quiet tone for the rest when you want to make one point.
Variants
Details
What Bar chart brings along, read from the registry and the source.
- Category
- Charts
- Area
- Elements
- Variants
- 4
- Version
- 1.0.0 · since 1.0.0
- File
- components/ui/bar-chart.tsx
- Brings along
- utilskitchart
- npm packages
- None
- Languages
- Texts come through props
- Studio
- Adjust in code
- Build with AI
- Over MCP in Claude, Cursor, Copilot and Codex
Props
The BarChartProps interface from components/ui/bar-chart.tsx, with the comments from the code. Also takes every prop of ChartA11y.
datarequiredDatum[]Rows, one per category, e.g. { month: '2026-03', online: 21400, store: 9800 }
xKeyrequiredstringKey of the category. "2026-03" and "2026-03-14" are read as dates and formatted per language.
seriesrequiredChartSeries[]One bar per series and category, in slot order (accent, grey, two accent tones)
formatoptionalIntl.NumberFormatOptionsValue format, e.g. { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }
xFormatoptionalIntl.DateTimeFormatOptionstooltipXFormatoptionalIntl.DateTimeFormatOptionsstackedoptionalbooleanSeveral series side by side (default) or stacked to a total
orientationoptional'vertical' | 'horizontal'vertical columns (default) or horizontal bars with the labels on the left
valueLabelsoptionalbooleanValue at the bar end. Single series and stacked totals, only where it fits.
highlightoptionalstringSingle series: the category (x value) that keeps the accent, the others step back
heightoptionalnumberVertical: plot height in px, axis labels included
rowHeightoptionalnumberHorizontal: height of one row in px
legendoptionalbooleanLegend above the plot. Default: for two series and more.
xLabeloptionalstringHeader of the first table column
Accessibility
Read from the source of the component.
- Needs no client state. The server writes it in full, so it reads without JavaScript too.
Motion
- With reduced motion (prefers-reduced-motion) it stays still. 1 places in the code switch motion off then.
- No animations, it stands still from the start.
Source
The file aiw add puts into your project. From then on it is yours.
Show the file, 144 lines
'use client';
import { round } from '@/lib/kit';
import { CartesianFrame, barPath, numberFormatter, rampColor, seriesColor, seriesOpacity, textWidth, useIntlLocale, type ChartA11y, type ChartSeries, type Datum } from '@/components/ui/chart';
export interface BarChartProps extends ChartA11y {
/** Rows, one per category, e.g. { month: '2026-03', online: 21400, store: 9800 } */
data: Datum[];
/** Key of the category. "2026-03" and "2026-03-14" are read as dates and formatted per language. */
xKey: string;
/** One bar per series and category, in slot order (accent, grey, two accent tones) */
series: ChartSeries[];
/** Value format, e.g. { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 } */
format?: Intl.NumberFormatOptions;
xFormat?: Intl.DateTimeFormatOptions;
tooltipXFormat?: Intl.DateTimeFormatOptions;
/** Several series side by side (default) or stacked to a total */
stacked?: boolean;
/** vertical columns (default) or horizontal bars with the labels on the left */
orientation?: 'vertical' | 'horizontal';
/** Value at the bar end. Single series and stacked totals, only where it fits. */
valueLabels?: boolean;
/** Single series: the category (x value) that keeps the accent, the others step back */
highlight?: string;
/** Vertical: plot height in px, axis labels included */
height?: number;
/** Horizontal: height of one row in px */
rowHeight?: number;
/** Legend above the plot. Default: for two series and more. */
legend?: boolean;
/** Header of the first table column */
xLabel?: string;
className?: string;
}
const GAP = 2;
const MAX = 24;
/**
* Columns or bars: simple, grouped, stacked and horizontal.
* <BarChart data={rows} xKey="month" series={[{ key: 'online', label: 'Online' }, { key: 'store', label: 'Store' }]} stacked />
*/
export function BarChart({ data, xKey, series, format, xFormat, tooltipXFormat, stacked = false, orientation = 'vertical', valueLabels, highlight, height = 260, rowHeight = 40, legend, xLabel, label, summary, labels, animate, className }: BarChartProps) {
const intl = useIntlLocale();
const fmt = numberFormatter(intl, format);
const horizontal = orientation === 'horizontal';
const num = (d: Datum, k: string) => (typeof d[k] === 'number' ? (d[k] as number) : 0);
const totals = data.map((d) => series.reduce((a, s) => a + Math.max(0, num(d, s.key)), 0));
const all = data.flatMap((d) => series.map((s) => num(d, s.key)));
const extent: [number, number] = stacked ? [0, Math.max(0, ...totals)] : [Math.min(0, ...all), Math.max(0, ...all)];
const single = series.length === 1;
const tipValue = (i: number) => (stacked ? totals[i] : single ? num(data[i], series[0].key) : null);
const labelW = valueLabels ? Math.max(0, ...data.map((_, i) => { const v = tipValue(i); return v === null ? 0 : textWidth(fmt(v), 12); })) + 10 : 0;
const plotHeight = horizontal ? data.length * rowHeight + 30 : height;
const dim = (i: number, si: number) => (highlight !== undefined && single && String(data[i][xKey]) !== highlight ? rampColor(1) : seriesColor(si, series[si].color));
return (
<CartesianFrame
data={data}
xKey={xKey}
series={series}
format={format}
xFormat={xFormat}
tooltipXFormat={tooltipXFormat}
height={plotHeight}
scale="band"
horizontal={horizontal}
extent={extent}
legend={legend}
legendShape="rect"
padRight={horizontal ? labelW : 0}
padTop={!horizontal && valueLabels ? 12 : 0}
total={stacked}
xLabel={xLabel}
label={label}
summary={summary}
labels={labels}
animate={animate}
className={className}
marks={({ cat, band, val, animated, n, active, focus }) => {
const zero = val(0);
const thick = stacked || single ? Math.min(MAX, band * (horizontal ? 0.56 : 0.6)) : Math.min(MAX, (band * 0.72 - GAP * (series.length - 1)) / series.length);
const groupW = stacked || single ? thick : thick * series.length + GAP * (series.length - 1);
const delay = (i: number) => (animated ? ({ '--kitch-d': `${round((i / Math.max(1, n)) * 420, 0)}ms` } as React.CSSProperties) : undefined);
return data.map((d, i) => {
const c = cat(i);
const start = round(c - groupW / 2);
const faded = active !== null && active !== i;
let marks: React.ReactNode;
if (stacked) {
let acc = 0;
const visible = series.map((s, si) => ({ s, si, v: Math.max(0, num(d, s.key)) })).filter((x) => x.v > 0);
marks = visible.map(({ s, si, v }, k) => {
const a = val(acc);
acc += v;
const b = val(acc);
const isTop = k === visible.length - 1;
// The 2px surface gap sits on top of every segment but the last
const len = Math.abs(b - a) - (isTop ? 0 : GAP);
if (len <= 0) return null;
const path = horizontal ? barPath(a, start, len, thick, isTop ? 4 : 0, 'right') : barPath(start, b + (isTop ? 0 : GAP), thick, len, isTop ? 4 : 0, 'up');
return <path key={s.key} d={path} fill={seriesColor(si, s.color)} className="kitch-series" opacity={seriesOpacity(si, focus)} />;
});
} else {
marks = series.map((s, si) => {
const v = num(d, s.key);
const p = val(v);
const off = round(start + si * (thick + GAP));
const len = Math.abs(p - zero);
if (len < 0.5) return null;
const path = horizontal ? barPath(Math.min(p, zero), off, len, thick, 4, v >= 0 ? 'right' : 'left') : barPath(off, Math.min(p, zero), thick, len, 4, v >= 0 ? 'up' : 'down');
return <path key={s.key} d={path} fill={dim(i, si)} className="kitch-series" opacity={seriesOpacity(si, focus)} />;
});
}
const tip = tipValue(i);
const text = valueLabels && tip !== null ? fmt(tip) : null;
const fits = text && (horizontal || textWidth(text, 11) <= band - 2);
const highlighted = highlight !== undefined && String(d[xKey]) === highlight;
return (
<g key={i} className="transition-opacity duration-200 motion-reduce:transition-none" opacity={faded ? 0.55 : 1}>
<g className={animated ? (horizontal ? 'kitch-grow-x' : tip !== null && tip < 0 ? 'kitch-grow-y-down' : 'kitch-grow-y') : undefined} style={delay(i)}>
{marks}
</g>
{fits && (
<text
x={horizontal ? val(tip!) + 8 : c}
y={horizontal ? c : val(Math.max(0, tip!)) - 7}
dy={horizontal ? '0.34em' : undefined}
textAnchor={horizontal ? 'start' : 'middle'}
className={`tabular-nums ${horizontal ? 'text-[12px]' : 'text-[11px]'} ${animated ? 'kitch-rise' : ''}`}
fontWeight={highlighted ? 600 : 500}
fill={highlighted || horizontal ? 'var(--foreground)' : 'var(--muted-foreground)'}
style={animated ? ({ '--kitch-d': `${round(380 + (i / Math.max(1, n)) * 420, 0)}ms` } as React.CSSProperties) : undefined}
>
{text}
</text>
)}
</g>
);
});
}}
/>
);
}Frequent questions
How do I install Bar chart?
With aiw add @ai-website/bar-chart. The command puts components/ui/bar-chart.tsx into your project, the code is yours from then on. It brings along chart. It needs no further npm packages.
Which variants does Bar chart have?
4 variants, Simple, Grouped, Stacked and Horizontal.
Can an AI assistant build with Bar chart?
Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Bar chart with aiw_components and install it with aiw_add @ai-website/bar-chart.