Siteplane UI

Bar Chart

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

Markdown
Jan to Dec. Signups: lowest 412, highest 905.
"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

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

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

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

Usage

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.

Jan to Jun. Opened: lowest 248, highest 334. Resolved: lowest 231, highest 318.
  • Opened
  • Resolved
"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.

Jan to Jun. New: lowest 184, highest 295. Reactivated: lowest 26, highest 40. Churned: lowest -77, highest -49.
  • New
  • Reactivated
  • Churned
"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.

Dashboards to Integrations. Active users: lowest 430, highest 2,140.
"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.

Jan to Jun. Net cash flow: lowest -$3.2K, highest $12.4K.
"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.

Jan to Jun. North America: lowest 176,900, highest 224,800. Europe: lowest 141,200, highest 171,400. Asia Pacific: lowest 76,800, highest 107,200.
  • North America
  • Europe
  • Asia Pacific
"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.

Jan to Jun. Online: lowest 1,240, highest 1,755. In store: lowest 790, highest 1,010.
  • Online
  • In store
"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.

Thu to Today. Search: lowest 197, highest 538. Direct: lowest 128, highest 351.
  • Search
  • Direct
"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.

Signups
January to June, by plan
Jan to Jun. Trial: lowest 412, highest 652. Paid: lowest 118, highest 203.
  • Trial
  • Paid
"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.

Apr to Sep. Orders: lowest 1,210, highest 1,450.
"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

Component properties
PropTypeDefaultDescription
BarChart.datareadonly ChartDatum[]requiredRows of the chart: an x value and one number per series.
BarChart.xstringrequiredKey of the x value in each row: category text, or a Date, shown as a category in date order.
BarChart.seriesreadonly ChartSeries[]requiredThe series to draw, in order.
BarChart.labelstringrequiredAccessible name of the chart.
BarChart.aria-describedbystringunsetId of an element that describes the chart, read after the summary of the data that the chart gives screen readers.
BarChart.orientationvertical | horizontalverticalvertical 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.stackedbooleanfalseStacks 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.heightnumber | fill240Height 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.minHeightnumber240With height fill, the least height of the chart in pixels, the legend included.
BarChart.valueLabelsbooleanfalseWrites 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.highlightChartXValueunsetThe 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.palettebrand | categoricalbrandbrand shades the brand color from strong to light; categorical gives each series its own chart color. A series’ own color wins either way.
BarChart.legendbooleantrue with more than one seriesLists the series below the plot.
BarChart.gridbooleantrueGrid lines at the value axis ticks: across the plot, or up and down in a horizontal chart.
BarChart.xAxisbooleantrueLabels along the x axis.
BarChart.yAxisbooleantrueLabels along the value axis.
BarChart.formatValue(value: number) => stringlocale number (1,234.5); compact on the axis (1.2K)Formats values in the tooltip and on the value axis.
BarChart.formatX(value: ChartXValue) => stringtext as given; a date as Jan 5, and Jan 5, 2026 in the tooltipFormats x values on the axis and in the tooltip heading.
BarChart.localestringen-USLocale of numbers and dates.
BarChart.emptyLabelstringNo dataShown in place of the marks when there is nothing to draw.
BarChart.labelsPartial<CartesianChartLabels>English wordsReplaces the words that the chart gives screen readers, each one on its own. Pass their translation when the application is in another language.
BarChart.classNamestringunsetAdditional classes.
ChartSeries.keystringrequiredKey of the numbers of this series in each row.
ChartSeries.labelstringrequiredName shown in the legend and the tooltip.
ChartSeries.colorstringpalette colorA CSS color, such as var(--chart-3), that replaces the palette color of this series.
ChartSeries.negativeColorstringunsetA 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.roleDescriptionstringchartWhat screen readers call the chart.
CartesianChartLabels.keyboardHintstringUse 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.tostringtoJoins the first and the last x value in the summary of the data, as in Jan to Jun.
CartesianChartLabels.loweststringlowestComes before the lowest value of each series in the summary.
CartesianChartLabels.higheststringhighestComes 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.

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 or an Area Chart reads better. When readers must read exact values, show a 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 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. 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.