# Heatmap calendar

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

- Type: React chart component (Charts & Stats)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add heatmap-calendar`
- Docs: https://wingo-ui.com/components/heatmap-calendar

## Interaction

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.

## Installation

```bash
npx wingo-ui@latest add heatmap-calendar
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/heatmap-calendar` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/heatmap-calendar.tsx`

Wingo UI dependencies (installed automatically):

- [Chart kit](https://wingo-ui.com/components/chart-kit) ([Markdown](https://wingo-ui.com/components/chart-kit.md))
- [Date utils](https://wingo-ui.com/components/date-utils) ([Markdown](https://wingo-ui.com/components/date-utils.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useScrollEdges](https://wingo-ui.com/components/use-scroll-edges) ([Markdown](https://wingo-ui.com/components/use-scroll-edges.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `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. |
| `endDate?` | `string` | `""` | Iso day of the last column. Empty uses the newest date in data (never the clock, so the server and the browser agree). |
| `today?` | `string` | `""` | Iso day that gets the today ring. Empty uses the viewer's date, read after hydration. |
| `weeks?` | `number` | `53` | Week columns in the window, ending with the week of endDate. |
| `weekStartsOn?` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| -1` | `-1` | First 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. |
| `levels?` | `number` | `5` | Color steps including the empty level. Ignored when thresholds are given. |
| `thresholds?` | `number[]` |  | 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. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | 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. |
| `color?` | `string` | `""` | Any CSS color, a palette slot ("series-3") or a tone name. Replaces the tone. |
| `emptyColor?` | `string` | `""` | Fill 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. |
| `cellSize?` | `"sm" \| "md" \| "lg"` | `"md"` | 10 / 12 / 16px cells with 2 / 3 / 4px gaps. |
| `radius?` | `"none" \| "sm" \| "md" \| "full"` | `"sm"` | Cell corners: 0 / 2 / 3px or circles. The legend swatches follow. |
| `overflow?` | `"scroll" \| "fit"` | `"scroll"` | 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. |
| `showMonthLabels?` | `boolean` | `true` | Short month names above the first column of each month, skipped when two would collide. |
| `showWeekdayLabels?` | `boolean` | `true` | Monday, Wednesday and Friday in the locale's two-letter form, pinned while the rail scrolls. |
| `showLegend?` | `boolean` | `true` | The Less ... More scale under the grid. |
| `showTotal?` | `boolean` | `true` | The header sentence with the window's total (labels.total). |
| `showTooltip?` | `boolean` | `true` | The readout on hover, tap and keyboard focus. Cell labels and the table still carry every value when off. |
| `tooltipMode?` | `"auto" \| "float" \| "fixed"` | `"auto"` | float rides beside the day, fixed pins above the grid (never under a finger); auto is fixed on touch screens. |
| `value?` | `string \| null` |  | The selected day (iso) when you control it; it gets a 2px ring. Pair with onValueChange. |
| `defaultValue?` | `string \| null` | `"null"` | The day selected at first (YYYY-MM-DD), uncontrolled; null selects none. The playground offers a few sample days. |
| `onValueChange?` | `(date: string \| null) => void` |  | Called when a day is selected, or with null when the selection clears. |
| `onDateClick?` | `(date: string, datum: HeatmapDatum \| null) => void` |  | A click, a second tap or Enter on a day; datum is null for days without data. |
| `valueFormat?` | `ChartValueFormat = "number" \| "compact" \| "currency" \| "percent"` | `"number"` | How values print in the tooltip, the total, the cell labels and the table. |
| `currency?` | `string` | `"USD"` | ISO 4217 code for valueFormat currency ("$12,480"). |
| `decimals?` | `number` | `0` | Fraction 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 }) => string` |  | Replaces the built-in value formatting everywhere. |
| `locale?` | `string` | `""` | Locale for numbers, month and weekday names, dates and the first day of the week. Empty uses the UIProvider / LocaleProvider locale. |
| `animated?` | `boolean` | `true` | The column sweep on first view and the color crossfade on data changes. Reduced motion always shows the final state. |
| `loading?` | `boolean` | `false` | A skeleton in the grid's box and in the total. |
| `emptyState?` | `ReactNode` |  | Replaces the grid when data is empty. Without it an empty heatmap shows the zero grid and labels.noData. |
| `label?` | `string` | `""` | Accessible name of the figure and the grid, caption of the table. Empty uses labels.table. |
| `description?` | `string` | `""` | A one-sentence takeaway for screen readers. |
| `surface?` | `string` | `""` | The 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. |
| `actions?` | `ReactNode` |  | Trailing content of the header row: a period switch, a link. |
| `labels?` | `Partial<HeatmapCalendarLabels>` |  | Strings: less, more, total(count, weeks), cell(value, date, datum), empty(date), noData, table, month, valueName, activeDays, loading. English by default. |
| `classNames?` | `Partial<Record<HeatmapCalendarSlot, string>>` |  | Classes for header, total, actions, scroller, grid, cell, month, weekday, legend, tooltip and table. |

## Parts

### DEFAULT_HEATMAP_CALENDAR_LABELS (constant)

The 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 (function)

getHeatLevel(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 (type)

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

## See also

- [GitHub Activity](https://wingo-ui.com/components/github-activity) ([Markdown](https://wingo-ui.com/components/github-activity.md))
- [Calendar](https://wingo-ui.com/components/calendar) ([Markdown](https://wingo-ui.com/components/calendar.md))
- [Chart kit](https://wingo-ui.com/components/chart-kit) ([Markdown](https://wingo-ui.com/components/chart-kit.md))

---

Canonical: https://wingo-ui.com/components/heatmap-calendar
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
