# Bar Chart

Compare values across categories as columns or horizontal bars, side by side or stacked.

Signups by month

[Complete TSX file](/examples/bar-chart/default.tsx)

```tsx
"use client";

import { BarChart } from "@/siteplane/components/bar-chart";

const signups = [
  { month: "Jan", signups: 412 },
  { month: "Feb", signups: 486 },
  { month: "Mar", signups: 538 },
  { month: "Apr", signups: 497 },
  { month: "May", signups: 621 },
  { month: "Jun", signups: 684 },
  { month: "Jul", signups: 652 },
  { month: "Aug", signups: 718 },
  { month: "Sep", signups: 803 },
  { month: "Oct", signups: 761 },
  { month: "Nov", signups: 842 },
  { month: "Dec", signups: 905 },
];

const series = [{ key: "signups", label: "Signups" }];

export default function Example() {
  return (
    <div className="w-full max-w-xl">
      <BarChart
        data={signups}
        label="Signups by month"
        series={series}
        x="month"
      />
    </div>
  );
}
```

A bar chart compares values across categories, such as months, plans or regions. Each series draws one bar per category, and the bars stand side by side or stacked, as columns or as horizontal bars. Hover or tap a category, or focus the chart and use the arrow keys, to read its values in a card that trails beside it.

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/bar-chart
```

With npm: `npx shadcn@4.18.0 add @siteplane/bar-chart`. [Registry JSON](/registry/bar-chart.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/card @siteplane/toggle-group
```

## Usage

```tsx
import { BarChart } from "@/siteplane/components/bar-chart";

<BarChart
  data={[
    { month: "Jan", signups: 412 },
    { month: "Feb", signups: 486 },
    { month: "Mar", signups: 538 },
  ]}
  label="Signups by month"
  series={[{ key: "signups", label: "Signups" }]}
  x="month"
/>;
```

## Multiple series

Pass several series to set their bars side by side in each category, in the order of the series. The legend is on by default when there is more than one series, and sits below the plot. A mouse over an entry steps the other series back; it does not toggle series.

Support tickets opened and resolved by month

[Complete TSX file](/examples/bar-chart/multiple.tsx)

```tsx
"use client";

import { BarChart } from "@/siteplane/components/bar-chart";

const tickets = [
  { month: "Jan", opened: 248, resolved: 231 },
  { month: "Feb", opened: 276, resolved: 262 },
  { month: "Mar", opened: 312, resolved: 289 },
  { month: "Apr", opened: 265, resolved: 271 },
  { month: "May", opened: 298, resolved: 305 },
  { month: "Jun", opened: 334, resolved: 318 },
];

const series = [
  { key: "opened", label: "Opened" },
  { key: "resolved", label: "Resolved" },
];

export default function Example() {
  return (
    <div className="w-full max-w-xl">
      <BarChart
        data={tickets}
        label="Support tickets opened and resolved by month"
        series={series}
        x="month"
      />
    </div>
  );
}
```

## Stacked

With `stacked`, the bars of a category stack in the order of the series: positive values up from 0 and negative values down from it. A missing value adds nothing, and the tooltip lists each series’ own value, not the sum.

Customers gained and lost by month

[Complete TSX file](/examples/bar-chart/stacked.tsx)

```tsx
"use client";

import { BarChart } from "@/siteplane/components/bar-chart";

const customers = [
  { month: "Jan", new: 184, reactivated: 26, churned: -58 },
  { month: "Feb", new: 203, reactivated: 31, churned: -64 },
  { month: "Mar", new: 241, reactivated: 28, churned: -71 },
  { month: "Apr", new: 226, reactivated: 35, churned: -49 },
  { month: "May", new: 268, reactivated: 40, churned: -77 },
  { month: "Jun", new: 295, reactivated: 37, churned: -62 },
];

const series = [
  { key: "new", label: "New" },
  { key: "reactivated", label: "Reactivated" },
  { key: "churned", label: "Churned" },
];

export default function Example() {
  return (
    <div className="w-full max-w-xl">
      <BarChart
        data={customers}
        label="Customers gained and lost by month"
        series={series}
        stacked
        x="month"
      />
    </div>
  );
}
```

## Horizontal

`orientation="horizontal"` puts the categories down the left side and the values along the bottom, the way a ranking reads. `xAxis` still labels the categories and `yAxis` the values, and the Up and Down keys step through the categories. The labels at the left take at most 40% of the chart's width, and a longer label ends there in an ellipsis. The tooltip names the category in full.

Active users by feature

[Complete TSX file](/examples/bar-chart/horizontal.tsx)

```tsx
"use client";

import { BarChart } from "@/siteplane/components/bar-chart";

const features = [
  { feature: "Dashboards", users: 2140 },
  { feature: "Reports", users: 1620 },
  { feature: "Exports", users: 1180 },
  { feature: "Alerts", users: 760 },
  { feature: "Integrations", users: 430 },
];

const series = [{ key: "users", label: "Active users" }];

export default function Example() {
  return (
    <div className="w-full max-w-xl">
      <BarChart
        data={features}
        label="Active users by feature"
        orientation="horizontal"
        series={series}
        x="feature"
      />
    </div>
  );
}
```

## Negative values

A value below 0 draws its bar on the other side of the 0 line. Give the series a `negativeColor` to color those bars and their tooltip rows; the bars above 0 keep the series color. Pass `formatValue` to format the values in the tooltip and on the value axis.

Net cash flow by month

[Complete TSX file](/examples/bar-chart/negative.tsx)

```tsx
"use client";

import { BarChart } from "@/siteplane/components/bar-chart";

const cashFlow = [
  { month: "Jan", net: 12400 },
  { month: "Feb", net: 8900 },
  { month: "Mar", net: -3200 },
  { month: "Apr", net: 6100 },
  { month: "May", net: -1800 },
  { month: "Jun", net: 9700 },
];

const series = [
  { key: "net", label: "Net cash flow", negativeColor: "var(--chart-2)" },
];

const dollars = new Intl.NumberFormat("en-US", {
  currency: "USD",
  maximumFractionDigits: 1,
  minimumFractionDigits: 0,
  notation: "compact",
  style: "currency",
});

export default function Example() {
  return (
    <div className="w-full max-w-xl">
      <BarChart
        data={cashFlow}
        formatValue={(value) => dollars.format(value)}
        label="Net cash flow by month"
        series={series}
        x="month"
      />
    </div>
  );
}
```

## Categorical palette

A series that names no `color` takes a shade of the brand color, from strong to light, spread over the number of series, up to eight. With `palette="categorical"`, each series takes its own chart color instead, `--chart-1` to `--chart-8`, which suits unrelated categories such as regions. Both palettes repeat after the eighth series. A `color` on a series always wins.

API requests by region and month

[Complete TSX file](/examples/bar-chart/categorical.tsx)

```tsx
"use client";

import { BarChart } from "@/siteplane/components/bar-chart";

const requests = [
  { month: "Jan", northAmerica: 182400, europe: 141200, apac: 76800 },
  { month: "Feb", northAmerica: 176900, europe: 148600, apac: 81300 },
  { month: "Mar", northAmerica: 194300, europe: 152100, apac: 88700 },
  { month: "Apr", northAmerica: 201700, europe: 158900, apac: 93400 },
  { month: "May", northAmerica: 213500, europe: 166200, apac: 99800 },
  { month: "Jun", northAmerica: 224800, europe: 171400, apac: 107200 },
];

const series = [
  { key: "northAmerica", label: "North America" },
  { key: "europe", label: "Europe" },
  { key: "apac", label: "Asia Pacific" },
];

export default function Example() {
  return (
    <div className="w-full max-w-xl">
      <BarChart
        data={requests}
        label="API requests by region and month"
        palette="categorical"
        series={series}
        x="month"
      />
    </div>
  );
}
```

## Value labels

Set `valueLabels` to write the value of each bar at its end: above a column, below a column under 0, and beside a horizontal bar. A stack shows its sum, and the sum of its negative part below it. The labels use `formatValue`, as the tooltip does, and the chart keeps room for them inside the plot, so the highest label stays in the chart. When the labels do not all fit, every k-th category keeps its labels, one k for the whole chart; bars side by side that are too thin for their labels show none. While new data glides in, the labels wait and fade in where the bars come to rest.

Orders by month with the total above each stack

[Complete TSX file](/examples/bar-chart/value-labels.tsx)

```tsx
"use client";

import { BarChart } from "@/siteplane/components/bar-chart";

const orders = [
  { month: "Jan", online: 1240, store: 860 },
  { month: "Feb", online: 1315, store: 790 },
  { month: "Mar", online: 1480, store: 905 },
  { month: "Apr", online: 1390, store: 940 },
  { month: "May", online: 1620, store: 1010 },
  { month: "Jun", online: 1755, store: 980 },
];

const series = [
  { key: "online", label: "Online" },
  { key: "store", label: "In store" },
];

const count = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0,
  minimumFractionDigits: 0,
});

export default function Example() {
  return (
    <div className="w-full max-w-xl">
      <BarChart
        data={orders}
        formatValue={(value) => count.format(value)}
        label="Orders by month and channel"
        series={series}
        stacked
        valueLabels
        x="month"
      />
    </div>
  );
}
```

## Highlighted category

`highlight` takes the x value of one category and sets it apart, such as a day that is still running. Its bars take a soft hatched fill, quiet stripes of their own color over a light tint of it, so each series keeps its color, and its label on the axis and its value labels take the text color. The highlight is a look only: name the category in the data, such as Today, because the tooltip and screen readers read it as any other.

Visitors this week with today still running

[Complete TSX file](/examples/bar-chart/highlight.tsx)

```tsx
"use client";

import { BarChart } from "@/siteplane/components/bar-chart";

const week = [
  { day: "Thu", search: 486, direct: 312 },
  { day: "Fri", search: 455, direct: 298 },
  { day: "Sat", search: 268, direct: 174 },
  { day: "Sun", search: 241, direct: 159 },
  { day: "Mon", search: 512, direct: 336 },
  { day: "Tue", search: 538, direct: 351 },
  { day: "Today", search: 197, direct: 128 },
];

const series = [
  { key: "search", label: "Search" },
  { key: "direct", label: "Direct" },
];

const count = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0,
  minimumFractionDigits: 0,
});

export default function Example() {
  return (
    <div className="w-full max-w-xl">
      <BarChart
        data={week}
        formatValue={(value) => count.format(value)}
        highlight="Today"
        label="Visitors per day by source, today so far"
        series={series}
        stacked
        valueLabels
        x="day"
      />
    </div>
  );
}
```

## Fill height

With `height="fill"`, the chart is as tall as its box, which takes its height from the container: a flex item that grows, a grid cell, or a box with a height of its own. The chart never goes lower than `minHeight`, 240 px by default, and keeps that height until it has measured its box, on the server too. Its own content never makes the box taller, so a card can hand the chart whatever room the rest of the card leaves.

A chart that fills the room its card leaves

[Complete TSX file](/examples/bar-chart/fill-height.tsx)

```tsx
"use client";

import { BarChart } from "@/siteplane/components/bar-chart";
import {
  Card,
  CardDescription,
  CardHeader,
  CardPanel,
  CardTitle,
} from "@/siteplane/components/card";

const signups = [
  { month: "Jan", trial: 412, paid: 118 },
  { month: "Feb", trial: 486, paid: 131 },
  { month: "Mar", trial: 538, paid: 156 },
  { month: "Apr", trial: 497, paid: 162 },
  { month: "May", trial: 604, paid: 187 },
  { month: "Jun", trial: 652, paid: 203 },
];

const series = [
  { key: "trial", label: "Trial" },
  { key: "paid", label: "Paid" },
];

const count = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0,
  minimumFractionDigits: 0,
});

export default function Example() {
  return (
    <Card className="h-[420px] w-full max-w-xl">
      <CardHeader>
        <CardTitle>Signups</CardTitle>
        <CardDescription>January to June, by plan</CardDescription>
      </CardHeader>
      <CardPanel className="flex flex-col">
        <BarChart
          className="flex-1"
          data={signups}
          formatValue={(value) => count.format(value)}
          height="fill"
          label="Signups by month and plan"
          minHeight={200}
          series={series}
          x="month"
        />
      </CardPanel>
    </Card>
  );
}
```

## Changing data

When `data` changes, the bars glide to it. Bars are told by category and series, so a new bar grows from 0, a bar that is gone shrinks away, and the bars that stay glide; keep the text or date of a category the same from one data set to the next. The value axis moves with the bars, and axis labels fade through.

Orders over the last 6 or 12 months

[Complete TSX file](/examples/bar-chart/changing.tsx)

```tsx
"use client";

import { useState } from "react";
import { BarChart } from "@/siteplane/components/bar-chart";
import {
  ToggleGroup,
  ToggleGroupItem,
} from "@/siteplane/components/toggle-group";

const orders = [
  { month: "Oct", orders: 1180 },
  { month: "Nov", orders: 1640 },
  { month: "Dec", orders: 1920 },
  { month: "Jan", orders: 960 },
  { month: "Feb", orders: 1010 },
  { month: "Mar", orders: 1130 },
  { month: "Apr", orders: 1210 },
  { month: "May", orders: 1290 },
  { month: "Jun", orders: 1340 },
  { month: "Jul", orders: 1260 },
  { month: "Aug", orders: 1380 },
  { month: "Sep", orders: 1450 },
];

const series = [{ key: "orders", label: "Orders" }];

export default function Example() {
  const [period, setPeriod] = useState(["6"]);
  const data = period[0] === "12" ? orders : orders.slice(-6);

  return (
    <div className="grid w-full max-w-xl gap-4">
      <ToggleGroup
        aria-label="Period"
        onValueChange={(next) => {
          if (next.length > 0) setPeriod(next);
        }}
        value={period}
        variant="outline"
      >
        <ToggleGroupItem value="6">6 months</ToggleGroupItem>
        <ToggleGroupItem value="12">12 months</ToggleGroupItem>
      </ToggleGroup>
      <BarChart data={data} label="Orders by month" series={series} x="month" />
    </div>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| BarChart.data | readonly ChartDatum[] | required | Rows of the chart: an x value and one number per series. |
| BarChart.x | string | required | Key of the x value in each row: category text, or a Date, shown as a category in date order. |
| BarChart.series | readonly ChartSeries[] | required | The series to draw, in order. |
| BarChart.label | string | required | Accessible name of the chart. |
| BarChart.aria-describedby | string | unset | Id of an element that describes the chart, read after the summary of the data that the chart gives screen readers. |
| BarChart.orientation | vertical \| horizontal | vertical | vertical draws columns over a category axis along the bottom; horizontal puts the categories down the left side and the values along the bottom, so the grid lines run up and down. xAxis still labels the categories and yAxis the values. |
| BarChart.stacked | boolean | false | Stacks the bars of a category in the order of the series: positive values up from 0, negative values down from it. Otherwise they stand side by side. |
| BarChart.height | number \| fill | 240 | Height of the chart in pixels, the legend included, or fill to take the height of its box down to minHeight; the width follows the container. |
| BarChart.minHeight | number | 240 | With height fill, the least height of the chart in pixels, the legend included. |
| BarChart.valueLabels | boolean | false | Writes each bar's value at its end, or the sum of each sign of a stack, in the format of formatValue. Every k-th category keeps its labels when they do not all fit. |
| BarChart.highlight | ChartXValue | unset | The x value of one category to set apart: its bars take a hatched fill of their color, and its axis label and value labels the text color. |
| BarChart.palette | brand \| categorical | brand | brand shades the brand color from strong to light; categorical gives each series its own chart color. A series’ own color wins either way. |
| BarChart.legend | boolean | true with more than one series | Lists the series below the plot. |
| BarChart.grid | boolean | true | Grid lines at the value axis ticks: across the plot, or up and down in a horizontal chart. |
| BarChart.xAxis | boolean | true | Labels along the x axis. |
| BarChart.yAxis | boolean | true | Labels along the value axis. |
| BarChart.formatValue | (value: number) => string | locale number (1,234.5); compact on the axis (1.2K) | Formats values in the tooltip and on the value axis. |
| BarChart.formatX | (value: ChartXValue) => string | text as given; a date as Jan 5, and Jan 5, 2026 in the tooltip | Formats x values on the axis and in the tooltip heading. |
| BarChart.locale | string | en-US | Locale of numbers and dates. |
| BarChart.emptyLabel | string | No data | Shown in place of the marks when there is nothing to draw. |
| BarChart.labels | Partial<CartesianChartLabels> | English words | Replaces the words that the chart gives screen readers, each one on its own. Pass their translation when the application is in another language. |
| BarChart.className | string | unset | Additional classes. |
| ChartSeries.key | string | required | Key of the numbers of this series in each row. |
| ChartSeries.label | string | required | Name shown in the legend and the tooltip. |
| ChartSeries.color | string | palette color | A CSS color, such as var(--chart-3), that replaces the palette color of this series. |
| ChartSeries.negativeColor | string | unset | A CSS color for the bars of this series below 0 and their tooltip rows. Without it, a bar keeps the series color whatever its sign. |
| CartesianChartLabels.roleDescription | string | chart | What screen readers call the chart. |
| CartesianChartLabels.keyboardHint | string | Use the arrow keys to move between data points. | Read after the description of the chart while it is a focus stop. An empty text leaves the hint out. |
| CartesianChartLabels.to | string | to | Joins the first and the last x value in the summary of the data, as in Jan to Jun. |
| CartesianChartLabels.lowest | string | lowest | Comes before the lowest value of each series in the summary. |
| CartesianChartLabels.highest | string | highest | Comes before the highest value of each series in the summary. |

BarChartProps exports these props. The chart core, `@/siteplane/components/chart`, installs with the Bar Chart and exports the shared types: ChartDatum, ChartSeries, ChartXValue, ChartPalette, CartesianChartLabels and CartesianChartProps. The chart renders a figure with the plot and, when it is shown, the legend; it takes no native props or ref beyond `className` and `aria-describedby`.

[Read the installed source](/source/bar-chart).

## Usage notes

Use a bar chart to compare a few to a few dozen categories, side by side, stacked or as a ranking. For a trend over more than about 30 dates, a [Line Chart](/docs/components/line-chart/) or an [Area Chart](/docs/components/area-chart/) reads better. When readers must read exact values, show a [Table](/docs/components/table/) instead: the axis gives a rough scale and the tooltip one category at a time. Several series share one value axis, which always includes 0.

On hover, focus or tap, the chart marks one category as active: the one under the pointer, or the one the keyboard has reached. A hairline crosshair runs through it, an inverse label on the axis names it, and in a column chart a dot per series glides to the end of its bar. The tooltip card sits beside the crosshair and trails it, and its values roll to the new ones. The other bars step back slightly, to 80%.

The plot is one focus stop. Focus from the keyboard marks the first category, and the arrow keys step through the categories: Left and Right, or Up and Down in a horizontal chart. Home and End jump to the ends, and Escape closes the tooltip. A tap or pen press pins a category until the next press elsewhere or Escape. Screen readers meet the plot as a chart named by `label`. Its description sums up the data with the first and the last category and the lowest and the highest value of each series; a chart with no value is described by `emptyLabel`. While the chart is a focus stop, a hint that the arrow keys move between the data points follows the description. `labels` replaces these words and the role description, for example with their translation. While the keyboard or a tap moves between the categories, a polite live region reads what the tooltip shows. The rows of the tooltip name each series, so the color never carries the information alone. A chart with no values is not a focus stop.

The chart has no click or selection API. New data animates with the [Motion](/docs/components/motion/) tokens (`--motion-duration-value` and `--motion-ease-standard`), and instantly under reduced motion: the system setting, `html` with `data-motion-reduce="true"` or `SiteplaneMotionProvider preset="reduced"`. The tooltip comes from [Tooltip](/docs/components/tooltip/). The chart opens and closes it itself, so it needs no `TooltipProvider`, which only shares hover delays between tooltips.

The chart draws its plot once it has measured the width of its container. Until then, and in the server HTML, it renders the box of the plot at its full height, the legend and what screen readers read, so nothing on the page moves when the plot appears. Text values on the x axis are categories, shown in the order given. Date values are shown in date order and written in UTC, so use dates at midnight UTC for days, such as `new Date("2026-01-05")`. A missing value leaves its bar out, and its series out of the tooltip for that category.
