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' }}
  formatY={{ locale: 'en-US', number: { notation: 'compact' } }}
  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

npx 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 '@/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:

const data = [
  { browser: 'chrome', visitors: 275 },
  { browser: 'safari', visitors: 200 },
]

<BarChart data={data} x="browser" y="visitors" series="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 '@/ui/chart'

const data = stackY(rows, { x: 'month', y: ['desktop', 'mobile', 'tablet'] })

<BarChart
  data={data}
  x="x"
  y="top"
  y1="base"
  series="series"
  seriesOrder={['desktop', 'mobile', 'tablet']}
/>

Call stackY at module scope — its result is the data identity. 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="group-y" groups the tooltip along the categories.

Multi-series wide data is grouped side by side by default. Set grouped={false} to let layers share the band, or grouped to group long-format series.

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} />

Both props are compared by identity, like data. A layer that depends on state belongs in a useMemo.

Accessibility

ariaLabel is required: a chart is a figure, not decoration. Describe what the bars measure, not the chart type.

The chart surface is keyboard-focusable. Tab moves to it, arrow keys walk the bars, Home and End jump to the ends, and Enter or Space pins the tooltip and fires onSelect. 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.

Examples

Default

Grouped

Stacked

Horizontal

Per-category color

Negative values

Labels

Labels inside bars

Highlight on focus

API Reference

BarChart

A bar chart. Field props (`x`, `y`, `series`, …) name keys of your row type and are checked against it. Every prop of `Chart` (`ariaLabel`, `height`, `className`, `children`, the focus callbacks) and every prop of `ChartBehaviorProps` (`focus`, `tooltip`, `animate`, …) is accepted too.

PropType
readonly Record<string, 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[]
string

Interaction and animation props are shared by every chart family:

Interaction and animation props shared by every chart family component. They are flat scalars on purpose: the chart definition is memoized on a serialized key, and a nested option object would silently go stale.

PropType
ChartFocus
number
ChartTooltipAnchor
boolean
false
ChartAnimate

The host props — height, width, className, and the focus callbacks — are documented on the Chart page.

Last updated on 8/1/2026