Heatmap
Contribution calendar or labelled matrix, lit by value, with one tooltip and arrow-key navigation. SSR-stable.
New in 1.7.
1,333 deploys to production in the last year
Busiest: 20 on Thursday, March 5
Installation
npx shadcn@latest add https://neonkit.heyaadi.com/r/heatmap.jsonUsage
import { Heatmap } from "@/components/ui/heatmap"
<Heatmap data={[{ date: "2026-10-16", value: 5 }]} end="2026-10-16" unit="deploy" />Examples
Matrix
rows, columns and values make a labelled grid. Here: failed CI runs by weekday and UTC hour, in five steps.
Range, week start and format
One quarter from Monday, larger cells, a fixed max so 90 minutes is the top step, and values written as minutes.
API reference
A role="grid" inside a div. Accepts div attributes; the ref points at the outer div.
| Prop | Type | Default |
|---|---|---|
data | HeatmapDatum[]Calendar mode: { date: "YYYY-MM-DD", value }. Entries on one date add up. | None |
start | stringFirst day shown. | end − 364 days |
end | stringLast day shown. Pass today's date from the server so the grid doesn't depend on the viewer's clock. | latest date in data |
weekStart | 00 starts each column on Sunday, 1 on Monday. | 0 |
formatDate | (date: string) => stringCalendar: how a day is named in the tooltip and for screen readers. | "Tue, Mar 3, 2026" |
rows | string[]Matrix mode: row labels. | None |
columns | string[]Matrix mode: column labels, shown above each column. | None |
values | (number | null)[][]Matrix mode: values[row][column]. null draws a hatched cell with no data. | None |
formatLabel | (row: string, column: string) => stringMatrix: how a cell is named. | "row, column" |
levels | numberLit steps above zero. The top step burns white-hot. | 4 |
max | numberThe value that reaches the top step. Values above it clamp. | largest value |
unit | stringWhat a value counts: "deploy" or ["failed run", "failed runs"]. Gives "14 deploys" and "No deploys". | None |
formatValue | (value: number) => stringReplaces the value text entirely. | None |
cellSize | numberLargest cell, in px. Cells shrink with the container. | 14 |
minCellSize | numberSmallest cell before the grid scrolls sideways: 8 in a calendar, 16 in a matrix so column labels fit. | 8 or 16 |
gap | numberSpace between cells, in px. | 3 |
legend | booleanShows Less ▢▢▢▢ More under the grid. | true |
legendLabels | [ReactNode, ReactNode]Ends of the legend. | ["Less", "More"] |
footer | ReactNodeLeft of the legend: a total, a caption, the selected day. | None |
value | stringSelected cell key: the date in a calendar, "row:column" indexes in a matrix. | None |
defaultValue | stringStarting selection when uncontrolled. | None |
onValueChange | (key: string | null, cell: HeatmapCell | null) => voidCalled on click, Enter or Space. Picking the selected cell again clears it. Passing any of these three props turns selection on. | None |
color | NeonColorThe scale's color. Falls back to --neon-accent, then cyan. | inherited |
Accessibility
- The grid is a single tab stop. Arrow keys move between cells; in a calendar Up and Down step through days and wrap into the next week. Home and End go to the ends of a row, Ctrl+Home and Ctrl+End to the first and last cell, Page Up and Page Down four columns.
- Every cell is named with its value and date ("14 deploys, Tue, Mar 3, 2026"). Selected cells report aria-selected only when selection is on.
- One tooltip serves the whole grid. It's portaled to the body, so a scrolling or clipped parent can't cut it off, and it follows keyboard focus as well as the pointer.
- Dates are read and formatted in UTC with fixed English names, so the server and the browser render the same grid. Pass formatDate for another language.
- When the calendar has to scroll, it opens scrolled to the latest weeks and the day labels stay pinned at the edge. They're backed in the page color, `#05060a`; on another surface, set `--nk-heat-surface` to match (`[--nk-heat-surface:#0b0d12]`).
- Only the top two steps throw light, and the glow is static. Hovering repaints one tooltip, not the grid.