Bar Chart

Compares categories with bars — grouped, stacked, horizontal, or labelled.

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-bar

It 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

  • ariaLabel is required and names the figure: describe what the bars measure, not the chart type. Add ariaDescription when the takeaway needs a sentence.
  • The chart surface is in the tab order. Arrow keys walk the bars, Home and End jump to the ends, Enter and Space pin the tooltip and fire onSelect, and Escape dismisses 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 labels so 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.

PropType
readonly unknown[]
string
readonly string[] | string
string
string
readonly string[]
Readonly<Record<string, string>>
string
boolean
boolean
number
number
number
boolean
boolean
boolean
ChartFormat
ChartFormat
readonly ChartMarkLayer[]
readonly ChartMarkLayer[]
union
string
string
number
'point' | 'pointer' | 'group-center'
boolean
boolean
ChartAnimate
number
number
number
number
string
ReactNode | function
ReactNode

Last updated on 9/7/2026