const data = /* ... */;
<BarChart
data={data}
x="month"
y={["desktop", "mobile"]}
labels={{ desktop: "Desktop", mobile: "Mobile" }}
ariaLabel="Desktop and mobile visitors per month, side by side"
/>BarChart plots one bar per category, or one bar per series inside each category band. It builds its own scales, axes, grid, legend, and tooltip from the fields you name, and paints series from your design system's chart palette.
Installation
pnpm dlx shadcn@latest add @dotui/chart-barIt brings the chart core — the host, the palette, and the shared frame — along with it.
Usage
import { BarChart } from "@/components/ui/chart-bar"const data = [
{ month: "Jan", desktop: 186, mobile: 80 },
{ month: "Feb", desktop: 305, mobile: 200 },
{ month: "Mar", desktop: 237, mobile: 120 },
]
export function Example() {
return (
<BarChart
data={data}
x="month"
y={["desktop", "mobile"]}
labels={{ desktop: "Desktop", mobile: "Mobile" }}
ariaLabel="Visitors per month"
/>
)
}x names the category field and y names one field per series. Series take palette slots — --chart-1, --chart-2, … — in the order you list them, and labels renames the keys wherever they surface: legend, tooltip, and screen readers.
Keep data out of the render body. It is compared by identity, so a fresh array on every render rebuilds the chart instead of animating it.
Wide and long data
Rows come in two shapes, and the chart reads both.
Wide rows carry one field per series — pass them as a y array, as above.
Long rows carry one value and name their own series. Pass a single y and a series field, and set seriesOrder to lead the color slots and the legend — series the data carries but seriesOrder omits follow in the order they first appear:
const data = [
{ browser: "chrome", visitors: 275 },
{ browser: "safari", visitors: 200 },
]<BarChart
data={data}
x="browser"
y="visitors"
series="browser"
ariaLabel="Visitors by browser"
/>Naming the category field as series is also how each bar gets its own color, as in the Per-category color example.
Stacked bars
stackY turns wide rows into long rows carrying the interval each segment spans. Feed it the same fields you would have passed to y, then plot top over base:
import { stackY } from "@/components/ui/chart"
const stacked = stackY(rows, { x: "month", y: ["desktop", "mobile", "tablet"] })<BarChart
data={stacked}
x="x"
y="top"
y1="base"
series="series"
seriesOrder={["desktop", "mobile", "tablet"]}
ariaLabel="Visitors per month by device, stacked"
/>Call stackY at module scope — its result is the data identity. Positive values stack up from zero and negative values down from it, so a mixed-sign group never overlaps itself. Add normalize: true for a 100% stack, and format the axis as a percentage with formatY={{ locale: "en-US", number: { style: "percent" } }}.
Orientation and grouping
horizontal moves the categories to the y axis and the values to x, which is what long category names want. The value axis is still x, so formatX formats the numbers, and focus groups along the categories automatically.
Multi-series wide data is grouped side by side by default. Set grouped to group long-format series too. Without grouping, long-format series that share a category stack from zero automatically, while wide-format series draw over one another — set y1 (as stackY does) when you want to control the interval yourself.
Labels and annotations
marks paints layers over the bars and marksBefore under them — any mark from @tanstack/charts, on the same scales:
import { text } from "@tanstack/charts/text"
// Same `z` as the bars, so grouped focus keeps one tooltip row per category.
const labels = [
text(data, {
x: "month",
y: "desktop",
text: "desktop",
z: () => "Desktop",
fill: "var(--color-fg-muted)",
dy: -10,
}),
]<BarChart
data={data}
x="month"
y="desktop"
marks={labels}
ariaLabel="Desktop visitors by month"
/>Both props are compared by identity, like data. A layer that depends on state belongs in a useMemo.
Accessibility
ariaLabelis required and names the figure: describe what the bars measure, not the chart type. AddariaDescriptionwhen the takeaway needs a sentence.- The chart surface is in the tab order. Arrow keys walk the bars,
HomeandEndjump to the ends,Enterand Space pin the tooltip and fireonSelect, andEscapedismisses it. Grouped focus reads one representative per category, so a grouped or stacked chart announces the whole category at once. - Color alone never carries the meaning — pass
labelsso every series has a name in the legend and tooltip, and keep the legend on when a chart has more than one series. tooltip={false}keeps the focus stops but removes their live region, so screen readers land on silent points. Turn it off only for a decorative chart.
Examples
Default
Grouped
Stacked
Horizontal
Per-category color
Negative values
Axes
Labels
Labels inside bars
Highlight on focus
API Reference
BarChart
Bar chart. Give it rows plus the fields to read: one `y` field per series for wide rows, or a single `y` with `series` for long rows.
| Prop | Type | Default | |
|---|---|---|---|
readonly unknown[] | — | ||
string | — | ||
readonly string[] | string | — | ||
string | — | ||
string | — | ||
readonly string[] | — | ||
Readonly<Record<string, string>> | — | ||
string | — | ||
boolean | false | ||
boolean | true for multi-series wide data, false otherwise | ||
number | 4 | ||
number | — | ||
number | — | ||
boolean | false | ||
boolean | true | ||
boolean | true | ||
ChartFormat | — | ||
ChartFormat | — | ||
readonly ChartMarkLayer[] | — | ||
readonly ChartMarkLayer[] | — | ||
union | "group-x", or "group-y" when `horizontal` | ||
string | — | ||
string | — | ||
number | — | ||
'point' | 'pointer' | 'group-center' | "group-center" | ||
boolean | true | ||
boolean | true | ||
ChartAnimate | { type: "spring", stiffness: 170, damping: 26 } | ||
number | 256 | ||
number | — | ||
number | — | ||
number | — | ||
string | — | ||
ReactNode | function | — | ||
ReactNode | — | ||
Last updated on 9/7/2026