Heatmap Chart
A contribution heatmap with animated cells, configurable level colors and patterns, and an interactive legend
Installation
pnpm dlx shadcn@latest add @bklit/heatmap-chartWhen 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.
| Prop | Type | Default | Description |
|---|---|---|---|
data | HeatmapColumn[] | required | One column per week (or category) with row bins inside |
xDomain | [Date, Date] | - | Visible time range — filters week columns |
sizingColumnCount | number | - | Column count for stable cell sizing when scrubbing |
layout | "fluid" | "fill" | "fluid" | fluid hugs content height; fill expands cells to parent |
margin | Partial<Margin> | { top: 28, right: 16, bottom: 0, left: 40 } | Chart margins |
binSize | number | 0 | Fixed cell size in px; 0 sizes cells to fit |
gap | number | 2 | Gap between cells in pixels |
colorScale | (count) => string | - | Override the default color scale |
levelColors | HeatmapLevelColors | --chart-scale-01 … --chart-scale-05 | Five colors for empty + four activity levels |
levelStyles | HeatmapLevelStyles | - | Per-level color and optional pattern; takes precedence over levelColors |
aspectRatio | string | - | CSS aspect ratio for the outer container |
status | ChartStatus | "ready" | Fetch / display status |
loadingLabel | string | - | Centered label while loading |
animationDuration | number | 1600 | Enter animation duration in ms |
animate | boolean | true | Play enter fade-in / loading shimmer |
columnSeparators | HeatmapSeparatorParsedConfig | - | Column grouping without a HeatmapSeparator child |
weekStartDay | 0–6 | 0 | First row of the grid (0 = Sunday, 1 = Monday, etc.) |
className | string | "" | Additional CSS class |
HeatmapCells
Renders the grid of cells with enter animation and hover dimming.
| Prop | Type | Default | Description |
|---|---|---|---|
cornerRadius | number | 2 | Corner radius for each cell |
colorScale | (count) => string | - | Override chart color scale |
inactiveOpacity | number | 0.3 | Opacity for inactive cells while hovering |
inactiveScale | number | 1 | Scale for inactive cells while hovering |
activeScale | number | 1 | Scale for the highlighted cell while hovering |
rowOpacity | number | number[] | 1 | Per-row cell opacity by display row index (0 = top row). See Row opacity. |
interactive | boolean | true | Pointer 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.
| Prop | Type | Default | Description |
|---|---|---|---|
tickFilter | "all" | "odd" | "even" | "odd" | Which row ticks to show |
labelFormat | "full" | "initial" | "full" | Full name or first letter |
rowOpacity | number | number[] | - | Per-row label opacity — pass the same value as HeatmapCells |
className | string | - | Additional class for labels |
HeatmapLegend
Less → More scale swatches that share levelStyles with the chart.
| Prop | Type | Default | Description |
|---|---|---|---|
lessLabel | string | "Less" | Label before swatches |
moreLabel | string | "More" | Label after swatches |
cellSize | number | 11 | Swatch size in pixels |
gap | number | 2 | Gap between swatches |
cornerRadius | number | 2 | Swatch corner radius |
variant | "swatches" | "gradient" | "swatches" | Discrete swatches or continuous gradient bar |
gradientSpan | number | 5 | Gradient bar width in swatch units |
fontSize | number | - | Font size in pixels for side labels |
labelClassName | string | - | Class name for less/more labels |
align | "start" | "center" | "end" | "end" | Horizontal alignment |
levelStyles | HeatmapLevelStyles | - | Shared level colors and patterns |
inactiveOpacity | number | 0.3 | Opacity for inactive swatches while interacting |
inactiveScale | number | 1 | Scale for inactive swatches while interacting |
activeScale | number | 1 | Scale for the highlighted swatch while interacting |
interactive | boolean | auto | Sync dimming with chart hover |
Gradient mode uses solid colors only (patterns are ignored).
HeatmapSeparator
Vertical column separators with optional quarter labels.
| Prop | Type | Default | Description |
|---|---|---|---|
groupBy | "every" | "quarter" | "every" | Group columns by interval or calendar quarter |
every | number | - | Insert a separator every N columns when groupBy="every" |
spacing | number | 0 | Horizontal gap between column groups in pixels |
startOffset | number | plot top | Distance from container top to line start (align with month labels) |
labelOffset | number | 0 | Distance below line top for quarter labels |
showLabels | boolean | false | Draw Q1–Q4 labels at group starts |
labelClassName | string | - | Class for quarter labels, e.g. "text-black dark:text-white" |
stroke | string | var(--border) | Line color when gradient is omitted |
strokeStyle | "solid" | "dashed" | "solid" | Line style |
strokeDasharray | string | "4,4" | Dash pattern when strokeStyle="dashed" |
strokeWidth | number | 1 | Line width in pixels |
strokeOpacity | number | 1 | Opacity multiplier for solid strokes or gradient stops |
gradient | HeatmapSeparatorGradient | - | 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. Fades in on first hover, springs between cells, and fades out on leave (position jumps and scale is skipped under prefers-reduced-motion).
| Prop | Type | Default | Description |
|---|---|---|---|
formatLabel | (count, date) => string | default formatter | Custom contribution line (bottom section) |
showDelay | number | 0 | Delay before first show (ms). Cell-to-cell updates are immediate once visible. |
hideDelay | number | 120 | Grace period before hiding when leaving a cell (ms). Reduces flicker between adjacent cells. |
instant | boolean | false | No fade/scale/spring motion — tooltip appears, moves and disappears immediately |
damping | number | 20 | Spring damping for the panel following the hovered cell (same scale as ChartTooltip). 0 snaps between cells |
springConfig | SpringConfig | - | Explicit follow spring (overrides damping); defaults to ChartConfigProvider's tooltipBoxSpring |
backgroundColor | string | var(--chart-tooltip-background) | Panel background (CSS variable or color value) |
panelStyle | CSSProperties | - | Inline styles for the tooltip panel |
className | string | "" | 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 row | Day |
|---|---|
| 0 | Mon |
| 1 | Tue |
| 2 | Wed |
| 3 | Thu |
| 4 | Fri |
| 5 | Sat |
| 6 | Sun |
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)Helper — buildHeatmapRowOpacity 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 rowsSingle 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.
Gallery
See the charts gallery for pattern fills and layout variants.