Data Storytelling

Stacked Column Chart

A stacked column chart React component. Clicking a legend item collapses that layer while the remaining layers sink into place on a spring and the Y axis rescales with tweened ticks; hover or use the arrow keys for a per-column breakdown.

A column chart that splits every column into layers so you can read its composition. The legend at the top has one toggle per layer (color dot, name, total across all columns). Clicking one collapses that layer to zero height and fades it out while the layers above sink into place on a spring; the Y axis re-rounds to the largest visible stack, grid lines glide to their new positions and tick numbers tween to their new values. Hovering a column dims the others to half opacity and shows a tooltip listing each layer's value and share, top to bottom in stack order. It is hand-drawn SVG with no charting library, and its width follows the container.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add stacked-column-chart.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

300
260
600
Active edited usageDefault props
<StackedColumnChart/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/stacked-column-chart.en.json

Or, once registries are configured in components.json, install it as @webberui/stacked-column-chart.

Usage

import { StackedColumnChart } from "@/components/ui/stacked-column-chart";

const series = [
  { key: "web", label: "官網" },
  { key: "store", label: "門市" },
  { key: "group", label: "團購" },
];

const data = [
  { label: "3 月", values: { web: 42, store: 31, group: 12 } },
  { label: "4 月", values: { web: 46, store: 29, group: 16 } },
  { label: "5 月", values: { web: 51, store: 33, group: 22 } },
];

<StackedColumnChart
  series={series}
  data={data}
  formatValue={(v) => `${v} 萬`}
  onHiddenKeysChange={(keys) => console.log("隱藏:", keys)}
/>

Props

PropTypeDefaultDescription
seriesStackedColumnSeries[]—Layer definitions, stacked bottom to top in order
dataStackedColumnDatum[]—Data for each column
hiddenKeysstring[]—Controlled hidden layer keys; managed internally when omitted
defaultHiddenKeysstring[][]Initially hidden layers in uncontrolled mode
onHiddenKeysChange(hiddenKeys: string[]) => void—Called when hidden layers change; at least one layer always stays visible
heightnumber300Plot height (px, including X labels); width follows the container
showTotalsbooleantrueWhether to show the visible-layer total above each column
formatValue(value: number) => stringgrouped digitsValue formatter shared by axis, totals and tooltip; add currency and units here
stiffnessnumber260Spring stiffness; when set it is written to --wb-stacked-column-chart-stiffness
durationnumber600Entrance grow duration (ms); when set it is written to --wb-stacked-column-chart-duration
ariaLabelstring"堆疊長條圖" (Stacked column chart)Accessible name used for the summary and the data table caption
classNamestring—Merged into the outermost container

StackedColumnSeries

FieldTypeDefaultDescription
keystring—Unique id; matches the keys of data[].values
labelstring—Name shown in the legend and tooltip
colorstring—Override this layer's color; defaults to --wb-stacked-column-chart-1 through -5

StackedColumnDatum

FieldTypeDefaultDescription
labelstring—X-axis label
valuesRecord<string, number>—Values per layer; missing or negative values count as 0

niceStackedScale(max) is also exported by name. It returns the same Y-axis max and ticks the chart uses, which helps when lining up other charts.

How it works

  • Colors come from CSS variables: --wb-stacked-column-chart-1 through -5 default to five teal steps from dark to light (#134e4a #0f766e #14b8a6 #5eead4 #ccfbf1), with a brighter set swapped in automatically in dark mode. Set those five variables on any ancestor to replace the whole palette; more than five layers cycle through it. The focus ring reads --wb-stacked-column-chart-accent (default #0d9488, #2dd4bf in dark mode).
  • Timing can be overridden too: --wb-stacked-column-chart-stiffness (default 260, damping fixed at 30) drives the spring for collapsing layers and moving grid lines, and --wb-stacked-column-chart-duration (default 600ms) drives the entrance growth. Columns are staggered by 40ms and tick numbers tween over 400ms.
  • A collapsed layer is not removed from the DOM. It shrinks to zero height and rests at the top of the stack below it, so when shown again it grows from that exact spot and the stack order never changes.
  • At least one layer always stays visible: when only one is left its legend button gets aria-disabled and clicking does nothing; if controlled mode passes "all hidden", the first layer still shows.
  • The entrance only plays once, when the chart scrolls into view (useInView with a -10% margin). The tooltip has a fixed width and is clamped inside the container so it never overflows the card.

Accessibility

  • Legend items are native button elements with aria-pressed reflecting whether the layer is visible; Enter / Space toggles them
  • The plot is focusable (tabIndex=0): ← / → move between columns, Home / End jump to the ends and Esc dismisses the tooltip; the current column's breakdown is announced through an aria-live region
  • The SVG has role="img" with an aria-label summary (column count, visible layers, highest total), plus a screen-reader-only data table with every layer's value and the visible total per column
  • Every focusable element shows a focus-visible ring
  • When the user has reduce motion enabled, column growth, layer collapsing and tick tweens jump straight to their final state, and the tooltip keeps working