Skip to content
skrewww

Content & Data

Bar Chart

Bar Chart compares categories with vertical or horizontal bars — a single series by default, or several series grouped, stacked, or stacked to 100%. Static and non-tabbable, with the data also exposed as a hidden table.

Part of the chart components — see the Charts overview for how the families and dashboard compositions fit together.

Single component — orientation and stacking are props, not variants

betaReact AvailableFigma PartialDocs Partial
React last updated
2026-09-22
Documentation last updated
2026-09-26
Accessibility target
WCAG 2.2 AA (target)
Version
0.1.0-beta

Known open questions

  • Figma covers only the single-series Bar Chart (example) static visual reference (bars + category labels); it is not a reusable Figma component. Multiple series, the value axis, grid, legend and tooltip have no Figma reference — they use current Skrewww tokens conservatively and need a design decision before parity can be claimed.
  • Horizontal orientation, stacking modes, the value axis, grid and tooltip likewise have no Figma reference; the Figma reference bars use radius/xs, but bar spacing and the React corner radius are this implementation's own decisions.
  • The categorical series palette (four slots: brand action, primary text, amber, green) is a CH-2 implementation choice checked for >= 3:1 contrast on the default white surface, not a Figma-verified palette. Slots carry no status meaning; positive/negative colors are a separate, unresolved design decision.
  • An interactive legend (series toggling) is deferred: it needs a hidden-series state plus keyboard and announcement design. The legend is a non-interactive key.
  • The tooltip is pointer/touch-only supplementary information; every value is also in the hidden table. A keyboard-operable chart (focusable points, arrow-key navigation) is deferred.
  • Dense data and long category labels have no policy yet: category labels always render (no tick skipping, rotation, truncation, scrolling or aggregation), so many points or long labels can collide or clip. A density and label-length policy is an open decision.
  • The horizontal category axis has a fixed width; long labels are not truncated or wrapped and can clip.

Purpose

Compare values across categories with bars: a single series by default, or several series grouped, stacked, or stacked to 100%, vertical or horizontal.

Anatomy

A recharts BarChart inside ResponsiveContainer (fluid width, fixed height — genuinely fills its parent). Each series is a Bar in its series color; slot 1 is semantic/action/primary, matching the Figma example. Category labels render on the category axis in semantic/text/secondary (axis and tick lines disabled); a value axis, grid, legend and tooltip are opt-in. orientation="horizontal" runs the category axis vertically; stacking="stacked" or "percent" stacks series into one bar per category. A visually-hidden (`sr-only`) data table — one column per series, missing values read "No data" — is rendered alongside, and the chart visual (plot, legend, tooltip) is aria-hidden inside a role="img" wrapper with aria-label + aria-describedby pointing at the table.

Variants and states

default

When to use

Comparing discrete categorical values, or the composition of several series per category (stacked / percent), where relative length matters more than exact precision.

When not to use

Trends over ordered time — use Line Chart or Area Chart. Parts of a single whole (Pie/Donut do not exist yet), distributions of raw points (Scatter is not available), or more than about four series (only four color slots exist; identification then relies on the legend and hidden table).

Accessibility

role="img" with an accessible name, described by a visually-hidden (sr-only) data table with one column per series (missing values read "No data"), so series are identified by name, never by color alone. The plot, legend and tooltip are aria-hidden; the chart is a static visualization with no keyboard tab stop, and the tooltip is pointer/touch-only supplementary information.

Common mistakes

Creating HorizontalBarChart / StackedBarChart components or props — use orientation="horizontal" and stacking="stacked" | "percent". Inventing props: chart families are separate components (do not add a `type`, `variant` or `stacked` prop), `series` keys must be unique and never "label", and raw Recharts props are not accepted. Pie, Donut, Scatter, Heatmap, Waterfall, Funnel and other chart families do not exist yet — do not import or claim them. Treating "static" as "no accessibility work needed": the hidden data table is required, not optional polish.

Properties

data (rows: { label, value } or { label, [series.key]: number | null }). label (accessible name / hidden table caption). height (fixed pixel height, default 240 — width is fluid, filling the parent). orientation ("vertical" columns, default | "horizontal"). stacking ("none" grouped, default | "stacked" | "percent"). series ({ key, label, color?, format? }[] — omit for a single series read from each row's `value`), legend (boolean, default: true for several series), tooltip (boolean, default false), showCategoryAxis, showValueAxis, showGrid (booleans), valueFormat (a function or { kind: "number" | "compact" | "percent" | "currency" }), labelFormat (a function or { kind: "date", granularity }).

Why ResponsiveContainer instead of fixed pixel dimensions?

A real consumer embeds this in a variable-width dashboard/card, so the chart should genuinely fill its parent. Height stays a fixed pixel value; width is fluid.

Why is there no Y-axis, grid, legend or tooltip by default?

Figma's own "Bar Chart (example)" frame shows only bars and category labels, so the default output matches it exactly. Richer chrome is opt-in (showValueAxis, showGrid, legend, tooltip) and has no Figma reference.

How is the underlying data exposed to screen readers?

A visually-hidden (sr-only) table with a column per series, linked to the chart via aria-describedby. Bar lengths and colors alone convey nothing to assistive tech, so this is a real accessibility mechanism, not optional polish.

Why is there no HorizontalBarChart, StackedBarChart or PercentStackedBarChart?

They are modes of one component: orientation="horizontal" and stacking="stacked" | "percent". Do not invent separate components or a `type`/`variant` prop.

Stacked or percent?

stacked shows absolute totals per category; percent scales every category to 100% to compare composition. In percent mode the value axis reads as a share, while the tooltip and table still show the raw values.

Tokens used

semantic/action/primarysemantic/text/primarysemantic/text/secondarysemantic/border/defaultsemantic/border/strongsemantic/surface/defaultsemantic/surface/subtlecolor/warning/700color/success/700

Component API

PropTypeDefaultDescription
data({ label: string } & { [seriesKey: string]: string | number | null })[]—One row per category: { label, value } for a single series, or { label, [series.key]: number | null } when `series` is given. null means no data (a gap or no bar). Duplicate labels are allowed.
labelstring—Accessible name for the chart — also used as the hidden data table's caption.
heightnumber240Fixed pixel height. Width is fluid (ResponsiveContainer), filling the parent.
series{ key: string; label: string; color?: "series-1" | "series-2" | "series-3" | "series-4"; format?: ChartValueFormat }[]—Series definitions: { key, label, color?, format? }. `key` is the row property holding the value (unique, never "label"); `color` is a slot "series-1"…"series-4" (default: by position, wrapping after four). Omit for a single series read from each row's `value`.
orientation"vertical" | "horizontal""vertical""vertical" draws columns; "horizontal" draws bars growing left to right with the category labels on the left.
stacking"none" | "stacked" | "percent""none""none" groups series side by side; "stacked" stacks them into one bar per category; "percent" stacks to 100% of each category.
legendboolean—Show a legend. Defaults to true when there is more than one series. It is a non-interactive key (no series toggling).
tooltipboolean—Show a hover/touch tooltip listing the category and each series' value. Defaults to false. Supplementary only: every value is also in the hidden table.
showCategoryAxisboolean—Show the category axis labels. Defaults to true.
showValueAxisboolean—Show the value-axis ticks. Defaults to false.
showGridboolean—Show grid lines. Defaults to false.
valueFormatChartValueFormat—How values are formatted on value-axis ticks, in the tooltip, and in the hidden table: a function (value: number) => string, or { kind: "number" | "compact" | "percent" | "currency", ... }. Default: the raw number. A series' own `format` overrides it.
labelFormatChartLabelFormat—How category labels are formatted on the axis, in the tooltip, and in the hidden table: a function (label: string) => string, or { kind: "date", granularity: "day" | "month" | "year" } for normalized ISO date labels (UTC). Default: the label as given.

React example

Copy React example
import { BarChart } from "@/components/ui";

const monthlySignups = [
  { label: "Jan", value: 58 },
  { label: "Feb", value: 95 },
  { label: "Mar", value: 76 },
  { label: "Apr", value: 128 },
  { label: "May", value: 108 },
  { label: "Jun", value: 140 },
];

const quarterly = [
  { label: "Q1", revenue: 120, orders: 80 },
  { label: "Q2", revenue: 150, orders: 95 },
  { label: "Q3", revenue: 135, orders: 110 },
];

export function Example() {
  return <BarChart data={monthlySignups} label="Monthly signups" />;
}

export function MultiSeriesExample() {
  return (
    <BarChart
      data={quarterly}
      label="Revenue and orders by quarter"
      series={[
        { key: "revenue", label: "Revenue" },
        { key: "orders", label: "Orders" },
      ]}
      stacking="stacked"
      showValueAxis
      showGrid
      tooltip
    />
  );
}