ComponentsBar Chart
Bar Chart
Compare values across categories or periods. Group series side by side to compare them directly, or stack them so each bar reads as a total. Bars always grow from zero.
On this page
Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.
View registry filesImplemented with React and browser APIs, without an additional behavior or rendering library.
Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.
Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.
Import the component into your page or component file.
import { BarChart } from "@dethink/components";Grouped series, a stacked chart with negative values, and a single series.
Grouped by channel
Hovering anywhere in a month highlights it and lists every channel; the bar under the pointer is emphasised in the tooltip and the other series dim.
Signups by channel, H1 2026
| Month | Organic | Paid | Referral |
|---|---|---|---|
| Jan | 1,240 | 610 | 180 |
| Feb | 1,310 | 680 | 210 |
| Mar | 1,480 | 720 | 260 |
| Apr | 1,390 | 910 | 240 |
| May | 1,620 | 880 | 310 |
| Jun | 1,750 | 940 | 330 |
Series: Organic, Paid, Referral. 6 points from Jan to Jun. Use the arrow keys, Home and End to read values.
Show sourceexamples/bar-chart/signups.tsx
"use client";
import { BarChart } from "@dethink/components";
const data = [
{ month: "Jan", organic: 1_240, paid: 610, referral: 180 },
{ month: "Feb", organic: 1_310, paid: 680, referral: 210 },
{ month: "Mar", organic: 1_480, paid: 720, referral: 260 },
{ month: "Apr", organic: 1_390, paid: 910, referral: 240 },
{ month: "May", organic: 1_620, paid: 880, referral: 310 },
{ month: "Jun", organic: 1_750, paid: 940, referral: 330 },
];
export function BarChartSignups() {
return (
<div className="w-full">
<h3 id="signups-heading" className="mb-3 text-sm font-medium">
Signups by channel, H1 2026
</h3>
<BarChart
aria-labelledby="signups-heading"
data={data}
index="month"
indexLabel="Month"
series={[
{ key: "organic", label: "Organic" },
{ key: "paid", label: "Paid" },
{ key: "referral", label: "Referral" },
]}
/>
</div>
);
}Stacked with negatives
Positive values stack up and negative values stack down from zero. Segments are separated by a 2px surface gap and only the outermost segment on each side is rounded.
New MRR movement by quarter
| Quarter | New | Expansion | Churn |
|---|---|---|---|
| Q1 25 | $84K | $31K | -$22K |
| Q2 25 | $91K | $36K | -$18K |
| Q3 25 | $78K | $42K | -$27K |
| Q4 25 | $102K | $47K | -$21K |
| Q1 26 | $110K | $52K | -$19K |
| Q2 26 | $118K | $58K | -$24K |
| Q3 26 | $125K | $61K | -$20K |
| Q4 26 | $131K | $66K | -$23K |
Series: New, Expansion, Churn. 8 points from Q1 25 to Q4 26. Use the arrow keys, Home and End to read values.
Show sourceexamples/bar-chart/net-revenue.tsx
"use client";
import { BarChart } from "@dethink/components";
const data = [
{ quarter: "Q1 25", new: 84_000, expansion: 31_000, churn: -22_000 },
{ quarter: "Q2 25", new: 91_000, expansion: 36_000, churn: -18_000 },
{ quarter: "Q3 25", new: 78_000, expansion: 42_000, churn: -27_000 },
{ quarter: "Q4 25", new: 102_000, expansion: 47_000, churn: -21_000 },
{ quarter: "Q1 26", new: 110_000, expansion: 52_000, churn: -19_000 },
{ quarter: "Q2 26", new: 118_000, expansion: 58_000, churn: -24_000 },
{ quarter: "Q3 26", new: 125_000, expansion: 61_000, churn: -20_000 },
{ quarter: "Q4 26", new: 131_000, expansion: 66_000, churn: -23_000 },
];
export function BarChartNetRevenue() {
return (
<div className="w-full">
<h3 id="net-revenue-heading" className="mb-3 text-sm font-medium">
New MRR movement by quarter
</h3>
<BarChart
aria-labelledby="net-revenue-heading"
stacked
data={data}
index="quarter"
indexLabel="Quarter"
series={[
{ key: "new", label: "New" },
{ key: "expansion", label: "Expansion" },
{ key: "churn", label: "Churn" },
]}
formatOptions={{ style: "currency", currency: "USD" }}
/>
</div>
);
}Single series
One series needs no legend. Bars stay at most 24px thick however wide the chart is.
Support tickets opened
| Day | Tickets |
|---|---|
| Mon | 142 |
| Tue | 168 |
| Wed | 155 |
| Thu | 171 |
| Fri | 139 |
| Sat | 64 |
| Sun | 58 |
Series: Tickets. 7 points from Mon to Sun. Use the arrow keys, Home and End to read values.
Show sourceexamples/bar-chart/tickets.tsx
"use client";
import { BarChart } from "@dethink/components";
const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const tickets = [142, 168, 155, 171, 139, 64, 58];
const data = days.map((day, i) => ({ day, tickets: tickets[i] }));
export function BarChartTickets() {
return (
<div className="w-full">
<h3 id="tickets-heading" className="mb-3 text-sm font-medium">
Support tickets opened
</h3>
<BarChart
aria-labelledby="tickets-heading"
data={data}
index="day"
indexLabel="Day"
height={200}
series={[{ key: "tickets", label: "Tickets", color: "chart-7" }]}
/>
</div>
);
}| Prop | What it does | Default |
|---|---|---|
dataArray<Record<string, unknown>> | One record per x position, in order. Non-numeric values leave a gap. | Not set |
indexstring | Record key holding the x value, such as "month". | Not set |
seriesArray<{ key, label, color? }> | Bars on one y-axis. color is a palette slot or CSS color; it defaults to the slot for the series' position, so filtering never repaints survivors. | Not set |
stackedboolean | One bar per index whose end reads as the total. Positive values stack up, negatives down. Tooltip, readout and table keep each series' own value. | false |
maxBarWidthnumber | Largest bar thickness in pixels; values above 24 are capped. Bars shrink to fit narrow charts. | 24 |
heightnumber | string | Plot height. Width always fills the container. | 288 |
yDomain[number, number] | Fix the value domain. The automatic domain always includes zero, because bar length encodes the value. | Not set |
formatValue / formatOptions(value) => string / Intl.NumberFormatOptions & { locale? } | Formats axis ticks, tooltip, readout and table values. | Compact en-US |
formatIndex / indexLabel(value, index) => string / string | Labels x values and names the table's first column. | String(value) / index |
legendboolean | Toggle buttons that show or hide series; hovering one dims the others. | series.length >= 2 |
hiddenSeries / defaultHiddenSeries / onHiddenSeriesChangestring[] / string[] / (hidden) => void | Controlled or uncontrolled series filtering. The last visible series cannot be hidden. | — / [] / — |
showTable / defaultShowTable / onShowTableChangeboolean / boolean / (show) => void | Swap the plot for a visible data table. The table is always present for assistive technology. | — / false / — |
tableToggleboolean | Render the Table view toggle button. | true |
loading / loadingLabel / emptyLabelboolean / ReactNode / ReactNode | Refetches keep the previous render at reduced opacity. | false / "Loading…" / "No data" |
animateboolean | Bars grow from zero on mount. Always off under reduced motion. | true |
BarChart shares Line Chart's accessible shell. The plot is a single tab stop named by aria-label or aria-labelledby. Arrow keys, Home and End move between categories, Escape clears the highlight, and each move is announced through a polite live region listing every series' own value. The data table is always present for assistive technology and can be shown with Table view. The legend, tooltip and table name every series, so identity never relies on color alone.