Skip to content
Dethink Components

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 files

Implemented 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.

Usage
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

Bar chart
MonthOrganicPaidReferral
Jan1,240610180
Feb1,310680210
Mar1,480720260
Apr1,390910240
May1,620880310
Jun1,750940330

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

Bar chart
QuarterNewExpansionChurn
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
examples/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

Bar chart
DayTickets
Mon142
Tue168
Wed155
Thu171
Fri139
Sat64
Sun58

Series: Tickets. 7 points from Mon to Sun. Use the arrow keys, Home and End to read values.

Show sourceexamples/bar-chart/tickets.tsx
examples/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>
  );
}
BarChart props
PropWhat it doesDefault
dataArray<Record<string, unknown>>One record per x position, in order. Non-numeric values leave a gap.Not set
indexstringRecord 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
stackedbooleanOne 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
maxBarWidthnumberLargest bar thickness in pixels; values above 24 are capped. Bars shrink to fit narrow charts.24
heightnumber | stringPlot 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 / stringLabels x values and names the table's first column.String(value) / index
legendbooleanToggle buttons that show or hide series; hovering one dims the others.series.length >= 2
hiddenSeries / defaultHiddenSeries / onHiddenSeriesChangestring[] / string[] / (hidden) => voidControlled or uncontrolled series filtering. The last visible series cannot be hidden.— / [] / —
showTable / defaultShowTable / onShowTableChangeboolean / boolean / (show) => voidSwap the plot for a visible data table. The table is always present for assistive technology.— / false / —
tableTogglebooleanRender the Table view toggle button.true
loading / loadingLabel / emptyLabelboolean / ReactNode / ReactNodeRefetches keep the previous render at reduced opacity.false / "Loading…" / "No data"
animatebooleanBars 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.