Skip to content

Charts

Stacked Column Chart

A vertical category comparison divided into consistently ordered segments.

When to use it: Use it for a small ordered set of categories where both totals and composition change.

On this page

Example

Loading example…
Stacked Column Chart.tsxtsx
import {
  ChartDescription,
  ChartTable,
  ChartTitle,
  ChartTooltip,
  StackedColumnChart,
  StackedColumnChartPlot,
  StackedColumnChartSegment,
} from "@comp0/react";

const signups = [
  {
    label: "Q1",
    segments: [
      { label: "Free", value: 44 },
      { label: "Pro", value: 18 },
    ],
  },
  {
    label: "Q2",
    segments: [
      { label: "Free", value: 52 },
      { label: "Pro", value: 24 },
    ],
  },
  {
    label: "Q3",
    segments: [
      { label: "Free", value: 49 },
      { label: "Pro", value: 31 },
    ],
  },
  {
    label: "Q4",
    segments: [
      { label: "Free", value: 61 },
      { label: "Pro", value: 38 },
    ],
  },
] as const;

const colors = [
  "bg-violet-600 fill-violet-600 dark:bg-violet-400 dark:fill-violet-400",
  "bg-amber-500 fill-amber-500 dark:bg-amber-400 dark:fill-amber-400",
] as const;

const formatSignups = (value: number) => `${value}k`;

export function Example() {
  return (
    <StackedColumnChart
      values={signups}
      categoryLabel="Quarter"
      valueLabel="Signups"
      formatValue={formatSignups}
      className="w-full max-w-2xl rounded-lg has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-4 has-[:focus-visible]:outline-violet-600 dark:has-[:focus-visible]:outline-violet-400"
    >
      <ChartTitle className="text-base font-semibold text-zinc-950 dark:text-zinc-50">
        Signups by plan
      </ChartTitle>
      <div className="mt-3 flex gap-4 text-sm">
        {signups[0].segments.map((segment, index) => (
          <span key={segment.label} className="flex items-center gap-2">
            <span aria-hidden="true" className={`size-3 rounded-sm ${colors[index]}`} />
            {segment.label}
          </span>
        ))}
      </div>
      <StackedColumnChartPlot
        aria-label="Stacked column chart showing quarterly signups by plan"
        className="mx-auto mt-5 aspect-square w-full max-w-md overflow-visible"
      >
        {(segment) => (
          <StackedColumnChartSegment
            key={`${segment.value.label}-${segment.segment.label}`}
            segment={segment}
            className="group outline-none"
          >
            <rect
              x={segment.x}
              y={segment.y}
              width={segment.width}
              height={segment.height}
              className={`${colors[segment.segmentIndex]} stroke-transparent group-data-active:stroke-zinc-950 dark:group-data-active:stroke-white`}
              strokeWidth="2"
              vectorEffect="non-scaling-stroke"
            />
          </StackedColumnChartSegment>
        )}
      </StackedColumnChartPlot>
      <ChartDescription className="mt-5 text-sm text-zinc-600 dark:text-zinc-400">
        Total signups peaked in the fourth quarter, with Pro accounting for a growing share.
      </ChartDescription>
      <ChartTable className="mt-4 w-full border-collapse text-left text-sm [&_td]:border-t [&_td]:border-zinc-200 [&_td]:py-2 [&_th]:border-zinc-200 [&_th]:py-2 dark:[&_td]:border-zinc-800 dark:[&_th]:border-zinc-800">
        <caption className="sr-only">Quarterly signups by plan</caption>
        <thead>
          <tr>
            <th scope="col">Quarter</th>
            <th scope="col">Free</th>
            <th scope="col">Pro</th>
            <th scope="col">Total</th>
          </tr>
        </thead>
        <tbody>
          {signups.map((quarter) => (
            <tr key={quarter.label}>
              <th scope="row">{quarter.label}</th>
              {quarter.segments.map((segment) => (
                <td key={segment.label}>{formatSignups(segment.value)}</td>
              ))}
              <td>
                {formatSignups(quarter.segments.reduce((sum, segment) => sum + segment.value, 0))}
              </td>
            </tr>
          ))}
        </tbody>
      </ChartTable>
      <ChartTooltip className="pointer-events-none z-50 rounded-md bg-zinc-950 px-2 py-1 text-sm text-white shadow-lg dark:bg-zinc-50 dark:text-zinc-950" />
    </StackedColumnChart>
  );
}

Anatomy

Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.

A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.

  1. StackedColumnChart

    Native figure sharing categories and consistently ordered segments. Owns a DOM element.

  2. ChartTitle

    Native figcaption that visibly names the figure. Owns a DOM element.

  3. StackedColumnChartPlot / StackedColumnChartSegment

    Vertical SVG columns divided into keyboard-reachable segments. Owns a DOM element.

  4. ChartDescription

    Visible prose summarizing totals and composition. Owns a DOM element.

  5. ChartTable

    Native table composed with segment columns and totals. Owns a DOM element.

  6. ChartTooltipOptional

    Optional floating segment label shown on hover or focus. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Start StackedColumnChart with categories that share the same ordered segment labels.

  2. 2

    Add the supporting parts

    Wrap each rendered section in StackedColumnChartSegment for category and segment navigation.

  3. 3

    Make the behavior clear

    Keep segment labels visible and include a native ChartTable with segment values and totals.

    Exampletsx
    <StackedColumnChart values={signups} categoryLabel="Quarter" valueLabel="Signups">
      <ChartTitle>Signups by plan</ChartTitle>
      <StackedColumnChartPlot aria-label="Stacked column chart of signups">
        {(segment) => (
          <StackedColumnChartSegment segment={segment}>
            <rect x={segment.x} y={segment.y} width={segment.width} height={segment.height} />
          </StackedColumnChartSegment>
        )}
      </StackedColumnChartPlot>
      <ChartTable>
        <caption>Signup values</caption>
      </ChartTable>
      <ChartTooltip />
    </StackedColumnChart>;

Keyboard

Enters the chart at its current segment and leaves with one more Tab.
Moves between categories while retaining the segment.
Moves between segments in the current category.
HomeEnd
Moves to the first or last segment.
Esc
Dismisses an open ChartTooltip.

Forms and accessibility

Charts are descriptive content and do not create form values.

Accessibility checklist

  • Keep segment labels visible next to color or pattern samples.
  • Wrap every section in StackedColumnChartSegment; horizontal arrows change category and vertical arrows change segment.
  • Announce each section's category, segment label, and formatted value.
  • Include every segment and total in a native table.
  • Do not rely on hue alone to distinguish adjacent sections.

API reference

Importtsx
import { ChartDescription, ChartTable, ChartTitle, ChartTooltip, StackedColumnChart, StackedColumnChartPlot, StackedColumnChartSegment } from "@comp0/react";

StackedColumnChart

DOM element

Native figure sharing categories and consistently ordered segments.

PropTypeDescription
valuesreadonly StackedChartValue[]Categories containing the same ordered, non-negative segments.
categoryLabelstringVisible category and numeric axis headings.
valueLabelstringVisible category and numeric axis headings.
formatValue(value: number) => stringFormats ticks, segment names, and table cells.

ChartTitle

DOM element

Native figcaption that visibly names the figure.

StackedColumnChartPlot / StackedColumnChartSegment

DOM element

Vertical SVG columns divided into keyboard-reachable segments.

PropTypeDescription
aria-labelstringConcise text alternative naming the comparison and composition.
yMaxnumberOptional numeric maximum and visible tick count.
yTickCountnumberOptional numeric maximum and visible tick count.
children(segment: StackedColumnChartSegmentState) => ReactNodeCustom segment renderer receiving category, segment, and geometry.
segmentStackedColumnChartSegmentStateSegment state passed from the plot to StackedColumnChartSegment.

ChartDescription

DOM element

Visible prose summarizing totals and composition.

ChartTable

DOM element

Native table composed with segment columns and totals.

ChartTooltip

OptionalDOM element

Optional floating segment label shown on hover or focus.

Style hooks

Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.

StackedColumnChartPlot / StackedColumnChartSegment

Style hookMeaning
[data-segment]The segment label represented by this section.
[data-active]The section currently reached by pointer or keyboard.

ChartTooltip

Style hookMeaning
[data-open]A value tooltip is visible.

Keep exploring