Heatmap calendar React chart component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React heatmap calendar of activity by day over weeks or a year, on a one-hue scale with month and weekday labels, a total and a legend.
The first time the heatmap scrolls into view its week columns fade and rise from left to right in one quick sweep. Hover a day for a readout of its date and value (the nearest day answers, so gaps never flicker) and click it to select it (click the selected day again to clear it). On a phone the year is a horizontal rail that opens at the newest week, with fading edges and week snapping while the weekday labels stay pinned; vertical drags still scroll the page. A first tap on a day pins its readout above the grid, a second tap on the same day selects it, a tap outside clears the readout. With the keyboard, Tab lands on one day (the selected one, else the newest): Left / Right move by a week, Up / Down by a day, Home / End jump to the first and last day shown, PageUp / PageDown move by four weeks, Enter or Space select (again to clear) and Escape clears the selection. Every day has a spoken label and a screen reader table lists each month's total. When the data changes the colors crossfade.
Heatmap calendar is a Pro React chart component in the React chart and stat components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add heatmap-calendar, together with Chart kit, Date utils, useControllableState, useScrollEdges, useLocale and 1 more.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add heatmap-calendarHeatmap calendar is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import HeatmapCalendar from @/components/ui/heatmap-calendar.
import { HeatmapCalendar, type HeatmapDatum } from "@/components/ui/heatmap-calendar"
const data: HeatmapDatum[] = [
{ date: "2026-09-21", value: 12 },
{ date: "2026-09-22", value: 9 },
{ date: "2026-09-23", value: 15 },
{ date: "2026-09-24", value: 7 },
]
export function TeamActivity() {
return (
<HeatmapCalendar
data={data}
endDate="2026-09-24"
locale="en-US"
label="Northwind CRM team activity"
labels={{
total: (n) => `${n} activities in the last year`,
cell: (value, date) => `${value} activities on ${date}`,
empty: (date) => `No activity on ${date}`,
}}
onDateClick={(date) => console.log(date)}
/>
)
}Props
data*HeatmapDatum[] = { date: string; value: number; label?: string }[]One entry per iso day ("2026-09-24"). Missing days count as 0, duplicate dates add up, days outside the window are ignored and negative values draw as 0. label names the value in the tooltip.
endDatedefault""stringIso day of the last column. Empty uses the newest date in data (never the clock, so the server and the browser agree).
todaydefault""stringIso day that gets the today ring. Empty uses the viewer's date, read after hydration.
weeksdefault53numberWeek columns in the window, ending with the week of endDate.
weekStartsOndefault-10 | 1 | 2 | 3 | 4 | 5 | 6 | -1First row of each column: 0 is Sunday, 1 Monday ... 6 Saturday; -1 follows the locale (Monday in en-GB, Sunday in en-US). Changes the row order and the weekday labels.
levelsdefault5numberColor steps including the empty level. Ignored when thresholds are given.
thresholdsnumber[]Lower bound of each level above 0, ascending: [1, 5, 10, 20] gives five levels. Default: quantiles of the non-zero values, so any activity is at least level 1.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The hue of the scale. Level 0 is the foreground at 7% (see emptyColor), then the tone from 30% to 100% in equal steps. neutral is the foreground ink.
colordefault""stringAny CSS color, a palette slot ("series-3") or a tone name. Replaces the tone.
emptyColordefault""stringFill of the days without activity (level 0 and its legend swatch): any CSS color or a palette slot. Empty uses the foreground at 7%, visible on white and on the dark surface.
cellSizedefault"md""sm" | "md" | "lg"10 / 12 / 16px cells with 2 / 3 / 4px gaps.
radiusdefault"sm""none" | "sm" | "md" | "full"Cell corners: 0 / 2 / 3px or circles. The legend swatches follow.
overflowdefault"scroll""scroll" | "fit"scroll: a horizontal rail that opens at the newest week, with edge fades and week snapping. fit: only the newest weeks that fit the width, nothing scrolls.
showMonthLabelsdefaulttruebooleanShort month names above the first column of each month, skipped when two would collide.
showWeekdayLabelsdefaulttruebooleanMonday, Wednesday and Friday in the locale's two-letter form, pinned while the rail scrolls.
showLegenddefaulttruebooleanThe Less ... More scale under the grid.
showTotaldefaulttruebooleanThe header sentence with the window's total (labels.total).
showTooltipdefaulttruebooleanThe readout on hover, tap and keyboard focus. Cell labels and the table still carry every value when off.
tooltipModedefault"auto""auto" | "float" | "fixed"float rides beside the day, fixed pins above the grid (never under a finger); auto is fixed on touch screens.
valuestring | nullThe selected day (iso) when you control it; it gets a 2px ring. Pair with onValueChange.
defaultValuedefault"null"string | nullThe day selected at first (YYYY-MM-DD), uncontrolled; null selects none. The playground offers a few sample days.
onValueChange(date: string | null) => voidCalled when a day is selected, or with null when the selection clears.
onDateClick(date: string, datum: HeatmapDatum | null) => voidA click, a second tap or Enter on a day; datum is null for days without data.
valueFormatdefault"number"ChartValueFormat = "number" | "compact" | "currency" | "percent"How values print in the tooltip, the total, the cell labels and the table.
currencydefault"USD"stringISO 4217 code for valueFormat currency ("$12,480").
decimalsdefault0numberFraction digits for values in the tooltip, the total and the table; auto by magnitude when unset (Auto in the playground).
formatValue(value: number, context: { kind: "tooltip" | "label" | "table" | "total"; date?: string }) => stringReplaces the built-in value formatting everywhere.
localedefault""stringLocale for numbers, month and weekday names, dates and the first day of the week. Empty uses the UIProvider / LocaleProvider locale.
animateddefaulttruebooleanThe column sweep on first view and the color crossfade on data changes. Reduced motion always shows the final state.
loadingdefaultfalsebooleanA skeleton in the grid's box and in the total.
emptyStateReactNodeReplaces the grid when data is empty. Without it an empty heatmap shows the zero grid and labels.noData.
labeldefault""stringAccessible name of the figure and the grid, caption of the table. Empty uses labels.table.
descriptiondefault""stringA one-sentence takeaway for screen readers.
surfacedefault""stringThe color behind the heatmap, painted under the pinned weekday labels and the today ring. Empty uses the surface token; set it when the heatmap sits on another background.
actionsReactNodeTrailing content of the header row: a period switch, a link.
labelsPartial<HeatmapCalendarLabels>Strings: less, more, total(count, weeks), cell(value, date, datum), empty(date), noData, table, month, valueName, activeDays, loading. English by default.
classNamesPartial<Record<HeatmapCalendarSlot, string>>Classes for header, total, actions, scroller, grid, cell, month, weekday, legend, tooltip and table.
Parts
DEFAULT_HEATMAP_CALENDAR_LABELS
constantThe English strings: Less and More, the total line, the cell and empty-day names, the no-data and loading text and the screen reader table's headers.
getHeatLevel
functiongetHeatLevel(value, thresholds) returns a cell's level: 0 for nothing, then one step per ascending threshold the value reaches. Use it to color a matching legend or list.
HeatmapDatum
typeOne day of data: { date, value, label? } with the date as YYYY-MM-DD; label names what the value counts in the tooltip.
Examples
GitHub look
tone success, radius sm, cellSize sm.
A quarter, large cells
weeks 13, cellSize lg, brand tone.
Fit vs scroll on a phone
Two 360px frames: the rail and the fitted grid.
Custom thresholds
Fixed bands [1, 5, 10, 20] instead of quantiles.
Selection drives a list
The selected day filters the activity log below.
Loading and empty
The skeleton, and the zero grid when there is no data.
Monday start, circles
en-GB week, a palette slot color, circles and formatValue.
Files
- components/ui/heatmap-calendar.tsx
FAQ
Is the React Heatmap calendar chart component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Heatmap calendar?
Run npx wingo-ui@latest add heatmap-calendar. It also adds the Wingo UI items it builds on (Chart kit, Date utils, useControllableState, useScrollEdges, useLocale and 1 more). With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/heatmap-calendar.
How does Heatmap calendar behave on phones?
On a phone the year is a horizontal rail that opens at the newest week, with fading edges and week snapping while the weekday labels stay pinned; vertical drags still scroll the page.
See also
Used in
More React chart and stat components
Articles using this component
- React GitHub Contribution Graph: Data, Themes and A11y
Component guides9 min read