Heatmap Chart

A contribution heatmap with animated cells, configurable level colors and patterns, and an interactive legend

Less
More

Installation

pnpm dlx shadcn@latest add @bklit/heatmap-chart

When using loadingLabel or HeatmapChartLoading, centered shimmer text uses @bklit/shimmering-text — installed automatically with @bklit/heatmap-chart.

Usage

The Heatmap Chart uses a composable API. Wrap the chart and legend in HeatmapInteractionProvider so hover dimming and tooltips stay in sync:

import {
  HeatmapCells,
  HeatmapChart,
  HeatmapInteractionBoundary,
  HeatmapInteractionProvider,
  HeatmapLegend,
  HeatmapTooltip,
  HeatmapXAxis,
  HeatmapYAxis,
} from "@bklitui/ui/charts";

const data = [
  {
    bin: 0,
    bins: [
      { bin: 0, count: 2, date: new Date(2024, 0, 1) },
      { bin: 1, count: 0, date: new Date(2024, 0, 2) },
      // …one week column with 7 day bins
    ],
  },
  // …one column per week
];

export default function ContributionHeatmap() {
  return (
    <HeatmapInteractionProvider>
      <HeatmapInteractionBoundary>
        <HeatmapChart data={data} layout="fluid">
          <HeatmapCells />
          <HeatmapXAxis />
          <HeatmapYAxis />
          <HeatmapTooltip />
        </HeatmapChart>
        <HeatmapLegend />
      </HeatmapInteractionBoundary>
    </HeatmapInteractionProvider>
  );
}

Components

HeatmapChart

The root component that sizes the grid, builds color scales, and provides context to children.

PropTypeDefaultDescription
dataHeatmapColumn[]requiredOne column per week (or category) with row bins inside
xDomain[Date, Date]-Visible time range — filters week columns
sizingColumnCountnumber-Column count for stable cell sizing when scrubbing
layout"fluid" | "fill""fluid"fluid hugs content height; fill expands cells to parent
marginPartial<Margin>{ top: 28, right: 16, bottom: 0, left: 40 }Chart margins
binSizenumber0Fixed cell size in px; 0 sizes cells to fit
gapnumber2Gap between cells in pixels
colorScale(count) => string-Override the default color scale
levelColorsHeatmapLevelColors--chart-scale-01--chart-scale-05Five colors for empty + four activity levels
levelStylesHeatmapLevelStyles-Per-level color and optional pattern; takes precedence over levelColors
aspectRatiostring-CSS aspect ratio for the outer container
statusChartStatus"ready"Fetch / display status
loadingLabelstring-Centered label while loading
animationDurationnumber1600Enter animation duration in ms
animatebooleantruePlay enter fade-in / loading shimmer
columnSeparatorsHeatmapSeparatorParsedConfig-Column grouping without a HeatmapSeparator child
weekStartDay060First row of the grid (0 = Sunday, 1 = Monday, etc.)
classNamestring""Additional CSS class

HeatmapCells

Renders the grid of cells with enter animation and hover dimming.

PropTypeDefaultDescription
cornerRadiusnumber2Corner radius for each cell
colorScale(count) => string-Override chart color scale
inactiveOpacitynumber0.3Opacity for inactive cells while hovering
inactiveScalenumber1Scale for inactive cells while hovering
activeScalenumber1Scale for the highlighted cell while hovering
rowOpacitynumber | number[]1Per-row cell opacity by display row index (0 = top row). See Row opacity.
interactivebooleantruePointer hover and dimming

When inactiveOpacity, inactiveScale, and activeScale are all 1, hover dimming is disabled.

HeatmapXAxis / HeatmapYAxis

Month labels along the top and weekday labels along the left. Y-axis labels follow weekStartDay on HeatmapChart.

PropTypeDefaultDescription
tickFilter"all" | "odd" | "even""odd"Which row ticks to show
labelFormat"full" | "initial""full"Full name or first letter
rowOpacitynumber | number[]-Per-row label opacity — pass the same value as HeatmapCells
classNamestring-Additional class for labels

HeatmapLegend

Less → More scale swatches that share levelStyles with the chart.

PropTypeDefaultDescription
lessLabelstring"Less"Label before swatches
moreLabelstring"More"Label after swatches
cellSizenumber11Swatch size in pixels
gapnumber2Gap between swatches
cornerRadiusnumber2Swatch corner radius
variant"swatches" | "gradient""swatches"Discrete swatches or continuous gradient bar
gradientSpannumber5Gradient bar width in swatch units
fontSizenumber-Font size in pixels for side labels
labelClassNamestring-Class name for less/more labels
align"start" | "center" | "end""end"Horizontal alignment
levelStylesHeatmapLevelStyles-Shared level colors and patterns
inactiveOpacitynumber0.3Opacity for inactive swatches while interacting
inactiveScalenumber1Scale for inactive swatches while interacting
activeScalenumber1Scale for the highlighted swatch while interacting
interactivebooleanautoSync dimming with chart hover

Gradient mode uses solid colors only (patterns are ignored).

HeatmapSeparator

Vertical column separators with optional quarter labels.

PropTypeDefaultDescription
groupBy"every" | "quarter""every"Group columns by interval or calendar quarter
everynumber-Insert a separator every N columns when groupBy="every"
spacingnumber0Horizontal gap between column groups in pixels
startOffsetnumberplot topDistance from container top to line start (align with month labels)
labelOffsetnumber0Distance below line top for quarter labels
showLabelsbooleanfalseDraw Q1–Q4 labels at group starts
labelClassNamestring-Class for quarter labels, e.g. "text-black dark:text-white"
strokestringvar(--border)Line color when gradient is omitted
strokeStyle"solid" | "dashed""solid"Line style
strokeDasharraystring"4,4"Dash pattern when strokeStyle="dashed"
strokeWidthnumber1Line width in pixels
strokeOpacitynumber1Opacity multiplier for solid strokes or gradient stops
gradientHeatmapSeparatorGradient-Optional vertical fade; omit for a flat solid stroke

Solid stroke — no gradient prop:

<HeatmapSeparator
  groupBy="quarter"
  showLabels
  labelClassName="text-black dark:text-white"
  stroke="var(--border)"
  strokeStyle="solid"
/>

Gradient fade — softens the top and bottom of each line:

<HeatmapSeparator
  groupBy="quarter"
  showLabels
  stroke="var(--muted)"
  gradient={{
    from: "var(--muted)",
    via: "var(--muted)",
    to: "var(--muted)",
    fromOpacity: 0,
    viaOpacity: 1,
    toOpacity: 0,
  }}
/>

HeatmapTooltip

Shows the contribution count and date for the hovered cell.

PropTypeDefaultDescription
formatLabel(count, date) => stringdefault formatterCustom contribution line (bottom section)
showDelaynumber0Delay before first show (ms). Cell-to-cell updates are immediate once visible.
hideDelaynumber120Grace period before hiding when leaving a cell (ms). Reduces flicker between adjacent cells.
instantbooleanfalseNo fade/scale motion — tooltip appears and disappears immediately
backgroundColorstringvar(--chart-tooltip-background)Panel background (CSS variable or color value)
panelStyleCSSProperties-Inline styles for the tooltip panel
classNamestring""Additional CSS class

Level styles and patterns

Pass levelStyles (five entries: empty + levels 1–4) to control both cell fills and legend swatches. Each level can be solid or use a pattern preset. Omit patternColor to derive stripe color from the level color, or set patternColor: "var(--chart-scale-pattern-color)" for the theme default:

const levelStyles = [
  { color: "var(--chart-scale-01)", fillMode: "solid" },
  { color: "var(--chart-scale-02)", fillMode: "solid" },
  { color: "var(--chart-scale-03)", fillMode: "pattern", pattern: "diagonal", patternColor: "var(--chart-scale-pattern-color)" },
  { color: "var(--chart-scale-04)", fillMode: "solid" },
  { color: "var(--chart-scale-05)", fillMode: "solid" },
] as const;

<HeatmapChart data={data} levelStyles={levelStyles}>
  <HeatmapCells />
</HeatmapChart>
<HeatmapLegend levelStyles={levelStyles} />

Data format

interface HeatmapBin {
  bin: number;       // row index (0–6 for days of week)
  count: number;     // activity level 0–4
  date: Date;
}

interface HeatmapColumn {
  bin: number;       // column index (week number)
  bins: HeatmapBin[];
}

Counts map to five visual levels (0 = empty, 1–4 = increasing activity). Use getHeatmapContributionLevel(count) from @bklitui/ui/charts to derive the level from a raw count.

Row opacity

rowOpacity on HeatmapCells and HeatmapYAxis controls opacity per display row — the row index after weekStartDay rotation, where 0 is the top row of the grid.

With weekStartDay={1} (Monday-first):

Display rowDay
0Mon
1Tue
2Wed
3Thu
4Fri
5Sat
6Sun

Pass the same value to HeatmapCells and HeatmapYAxis so faded rows and their labels stay aligned.

Plain array — explicit opacity per row:

const rowOpacity = [1, 1, 1, 1, 1, 0.35, 0.35]; // fade Sat + Sun (Mon-first)

HelperbuildHeatmapRowOpacity from @bklitui/ui/charts:

import { buildHeatmapRowOpacity } from "@bklitui/ui/charts";

// Fade rows 5 and 6 (Sat + Sun when weekStartDay={1})
const weekendOpacity = buildHeatmapRowOpacity([5, 6], 0.35);

// Same result with a row predicate (nth-style rule)
const weekendOpacity = buildHeatmapRowOpacity((row) => row >= 5, 0.35);

Other predicate examples:

buildHeatmapRowOpacity((row) => row % 2 === 0, 0.5); // even rows
buildHeatmapRowOpacity((row) => row < 2, 0.5);     // first two rows

Single number — applies to every row:

<HeatmapCells rowOpacity={0.6} />

Monday-first grid with faded weekends

Source data can stay Sunday-first; weekStartDay rotates display rows without reshaping your data:

import { buildHeatmapRowOpacity } from "@bklitui/ui/charts";

const weekendOpacity = buildHeatmapRowOpacity((row) => row >= 5, 0.35);

<HeatmapChart data={data} weekStartDay={1}>
  <HeatmapCells rowOpacity={weekendOpacity} />
  <HeatmapYAxis
    tickFilter="all"
    labelFormat="initial"
    rowOpacity={weekendOpacity}
  />
</HeatmapChart>

Default level colors use --chart-scale-01--chart-scale-05. See Theming.

See the charts gallery for pattern fills and layout variants.