Skip to content

Charts

Heatmap Chart

A two-dimensional category matrix whose cells encode a numeric magnitude.

When to use it: Use it to scan patterns across two categorical dimensions.

On this page

Example

Loading example…
Heatmap Chart.tsxtsx
import {
  ChartDescription,
  ChartTable,
  ChartTitle,
  ChartTooltip,
  HeatmapChart,
  HeatmapChartCell,
  HeatmapChartPlot,
} from "@comp0/react";

const weeks = ["W1", "W2", "W3", "W4", "W5", "W6", "W7", "W8", "W9", "W10"] as const;
const weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] as const;
const contributionCounts = [
  [0, 1, 3, 2, 0, 0, 0],
  [1, 4, 6, 3, 2, 0, 0],
  [0, 2, 5, 7, 4, 1, 0],
  [2, 6, 9, 5, 3, 0, 1],
  [1, 3, 8, 11, 6, 2, 0],
  [4, 8, 12, 9, 7, 1, 0],
  [3, 7, 10, 13, 8, 2, 1],
  [1, 5, 8, 6, 4, 0, 0],
  [0, 3, 5, 4, 2, 1, 0],
  [2, 4, 7, 5, 3, 0, 0],
] as const;
const contributions = weeks.flatMap((week, weekIndex) =>
  weekdays.map((weekday, weekdayIndex) => ({
    x: week,
    y: weekday,
    value: contributionCounts[weekIndex]![weekdayIndex]!,
  })),
);

const formatContributions = (value: number) =>
  value === 1 ? "1 contribution" : `${value} contributions`;

function contributionColor(value: number) {
  if (value >= 10) {
    return "fill-emerald-800 bg-emerald-800 dark:fill-emerald-300 dark:bg-emerald-300";
  }
  if (value >= 7) {
    return "fill-emerald-600 bg-emerald-600 dark:fill-emerald-500 dark:bg-emerald-500";
  }
  if (value >= 4) {
    return "fill-emerald-400 bg-emerald-400 dark:fill-emerald-700 dark:bg-emerald-700";
  }
  if (value >= 1) {
    return "fill-emerald-200 bg-emerald-200 dark:fill-emerald-900 dark:bg-emerald-900";
  }
  return "fill-zinc-100 bg-zinc-100 dark:fill-zinc-800 dark:bg-zinc-800";
}

export function Example() {
  return (
    <HeatmapChart
      values={contributions}
      xLabel="Week"
      yLabel="Weekday"
      valueLabel="Contributions"
      formatValue={formatContributions}
      className="w-full max-w-2xl rounded-lg has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-4 has-[:focus-visible]:outline-emerald-600 dark:has-[:focus-visible]:outline-emerald-400"
    >
      <ChartTitle className="text-base font-semibold text-zinc-950 dark:text-zinc-50">
        Contribution activity
      </ChartTitle>
      <HeatmapChartPlot
        aria-label="Contribution heatmap by weekday and week"
        className="mx-auto mt-5 aspect-square w-full max-w-lg overflow-visible"
      >
        {(cell) => (
          <HeatmapChartCell
            key={`${cell.value.x}-${cell.value.y}`}
            cell={cell}
            className="group outline-none"
          >
            <rect
              x={cell.x + 1}
              y={cell.y + 1}
              width={Math.max(0, cell.width - 2)}
              height={Math.max(0, cell.height - 2)}
              rx="1.5"
              className={`${contributionColor(cell.value.value)} stroke-transparent group-data-active:stroke-zinc-950 dark:group-data-active:stroke-white`}
              strokeWidth="2"
              vectorEffect="non-scaling-stroke"
            />
          </HeatmapChartCell>
        )}
      </HeatmapChartPlot>
      <div
        aria-label="Contribution intensity"
        className="mt-3 flex items-center justify-end gap-1 text-xs text-zinc-600 dark:text-zinc-400"
      >
        <span>Less</span>
        {[0, 2, 5, 8, 12].map((value) => (
          <span
            key={value}
            aria-hidden="true"
            className={`size-3 rounded-[2px] ${contributionColor(value)}`}
          />
        ))}
        <span>More</span>
      </div>
      <ChartDescription className="mt-5 text-sm text-zinc-600 dark:text-zinc-400">
        Activity peaks in weeks 6 and 7, with most contributions landing on weekdays.
      </ChartDescription>
      <div className="mt-4 overflow-x-auto">
        <ChartTable className="w-full min-w-[36rem] 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]:sr-only" />
      </div>
      <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" />
    </HeatmapChart>
  );
}

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

    Native figure sharing categorical coordinates, values, labels, and formatting. Owns a DOM element.

  2. ChartTitle

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

  3. HeatmapChartPlot / HeatmapChartCell

    SVG matrix with one keyboard-reachable rectangle per supplied coordinate. Owns a DOM element.

  4. ChartDescription

    Visible prose summarizing the strongest pattern. Owns a DOM element.

  5. ChartTable

    Native table that renders a complete default from the chart data: a caption from the ChartTitle text or axis labels, column headers, and one row per value. Pass children for a custom body. Owns a DOM element.

  6. ChartTooltipOptional

    Optional floating cell value shown on hover or focus. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Start HeatmapChart with unique x and y coordinate pairs and labels for both axes and the measured value.

  2. 2

    Add the supporting parts

    Wrap every rendered rectangle in HeatmapChartCell so arrow keys follow rows and columns.

  3. 3

    Make the behavior clear

    Use a labelled color scale, a native matrix table, and an optional ChartTooltip for exact values.

    Exampletsx
    <HeatmapChart values={contributions} xLabel="Week" yLabel="Weekday" valueLabel="Contributions">
      <ChartTitle>Contribution activity</ChartTitle>
      <HeatmapChartPlot aria-label="Contribution heatmap by weekday and week">
        {(cell) => (
          <HeatmapChartCell cell={cell}>
            <rect x={cell.x} y={cell.y} width={cell.width} height={cell.height} />
          </HeatmapChartCell>
        )}
      </HeatmapChartPlot>
      <ChartTable />
      <ChartTooltip />
    </HeatmapChart>;

Keyboard

⇥
Enters the chart at its current cell and leaves with one more Tab.
←→↑↓
Moves one available cell in the requested row or column direction.
HomeEnd
Moves to the first or last supplied cell.
Esc
Dismisses an open ChartTooltip.

Forms and accessibility

Charts are descriptive content and do not create form values.

Accessibility checklist

  • Keep both category axes visible and explain what the color intensity measures.
  • Wrap every custom rectangle in HeatmapChartCell for row-and-column arrow navigation.
  • Use a sequential palette with distinguishable contrast and never rely on color without numeric labels or a table.
  • Include a native table with real row and column headers.
  • ChartTooltip may reveal exact values visually but must not replace the table.

API reference

Importtsx
import { ChartDescription, ChartTable, ChartTitle, ChartTooltip, HeatmapChart, HeatmapChartCell, HeatmapChartPlot } from "@comp0/react";

HeatmapChart

DOM element

Native figure sharing categorical coordinates, values, labels, and formatting.

PropTypeDescription
valuesreadonly HeatmapChartValue[]Unique x and y label pairs with finite values.
xLabelstringVisible headings for both dimensions and the encoded measure.
yLabelstringVisible headings for both dimensions and the encoded measure.
valueLabelstringVisible headings for both dimensions and the encoded measure.
formatValue(value: number) => stringFormats cell names and table cells.

ChartTitle

DOM element

Native figcaption that visibly names the figure.

HeatmapChartPlot / HeatmapChartCell

DOM element

SVG matrix with one keyboard-reachable rectangle per supplied coordinate.

PropTypeDescription
aria-labelstringConcise text alternative naming the matrix and measure.
children(cell: HeatmapChartCellState) => ReactNodeCustom cell renderer receiving its coordinate, value, and geometry.
cellHeatmapChartCellStateCell state passed from the plot to HeatmapChartCell.

ChartDescription

DOM element

Visible prose summarizing the strongest pattern.

ChartTable

DOM element

Native table that renders a complete default from the chart data: a caption from the ChartTitle text or axis labels, column headers, and one row per value. Pass children for a custom body.

ChartTooltip

OptionalDOM element

Optional floating cell value shown on hover or focus.

PropTypeDescription
placementPopoverPlacementSide of the active mark; defaults to "top".
offsetnumberDistance from the active mark; defaults to eight pixels.
childrenReactNode | (details: ChartValueDetails) => ReactNodeCustom content receiving the active cell's formatted details.

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.

HeatmapChartPlot / HeatmapChartCell

Style hookMeaning
[data-x]The column label of this cell.
[data-y]The row label of this cell.
[data-value]The numeric value encoded by this cell.
[data-active]The cell currently reached by pointer or keyboard.

ChartTooltip

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

Keep exploring