Heatmap Calendar
GitHub-style contribution calendar: SVG cells, month labels, intensity bands, tooltips, optional range selection, comparison outline, streak summary, and responsive sizing.
Default
Full-year demo data with Monday-first weeks and tooltips on each day.
default-example.tsx
1"use client";23import { useMemo } from "react";4import {5 HeatmapCalendar,6 generateHeatmapDemoData,7} from "@/registry/ui/heatmap-calendar";89const DEMO_YEAR = 2026;1011export function HeatmapCalendarDefaultExample() {12 const data = useMemo(13 () =>14 generateHeatmapDemoData({15 startDate: `${DEMO_YEAR}-01-01`,16 endDate: `${DEMO_YEAR}-12-31`,17 density: 0.62,18 seed: 42,19 }),20 [],21 );2223 return (24 <HeatmapCalendar25 data={data}26 year={DEMO_YEAR}27 weekStartsOn={1}28 aria-label="Contributions in 2026"29 />30 );31}
Range selection
Controlled range with Shift+click; status line shows the ISO span.
Click a day to anchor, then Shift+click another day to select a range. Use arrow keys while the chart is focused to move between days; Enter confirms range when using keyboard.
Selected: 2026-03-01 → 2026-03-14On small screens the chart scrolls horizontally so day cells stay tappable.
range-example.tsx
1"use client";23import { useMemo, useState } from "react";4import {5 HeatmapCalendar,6 generateHeatmapDemoData,7 type HeatmapDateRange,8} from "@/registry/ui/heatmap-calendar";910const DEMO_YEAR = 2026;1112export function HeatmapCalendarRangeExample() {13 const data = useMemo(14 () =>15 generateHeatmapDemoData({16 startDate: `${DEMO_YEAR}-01-01`,17 endDate: `${DEMO_YEAR}-12-31`,18 density: 0.5,19 seed: 7,20 }),21 [],22 );2324 const [range, setRange] = useState<HeatmapDateRange | null>({25 start: `${DEMO_YEAR}-03-01`,26 end: `${DEMO_YEAR}-03-14`,27 });2829 return (30 <div className="w-full min-w-0 max-w-full space-y-2">31 <HeatmapCalendar32 data={data}33 year={DEMO_YEAR}34 weekStartsOn={1}35 selectionMode="range"36 selectedRange={range}37 onSelectRange={setRange}38 aria-label="Selectable contribution range"39 />40 <p className="text-xs text-muted-foreground">41 Selected:{" "}42 {range43 ? `${range.start} → ${range.end}`44 : "None - click a cell then Shift+click another"}45 <span className="mt-1 block sm:hidden">46 On small screens the chart scrolls horizontally so day cells stay47 tappable.48 </span>49 </p>50 </div>51 );52}
Comparison & playback
Animated reveal, comparison outline on matching dates, and streak summary.
Longest streak 0 days
rich-example.tsx
1"use client";23import { useEffect, useMemo, useState } from "react";4import {5 HeatmapCalendar,6 generateHeatmapDemoData,7 type HeatmapDay,8} from "@/registry/ui/heatmap-calendar";910const DEMO_YEAR = 2026;1112export function HeatmapCalendarRichExample() {13 const full = useMemo(14 () =>15 generateHeatmapDemoData({16 startDate: `${DEMO_YEAR}-01-01`,17 endDate: `${DEMO_YEAR}-12-31`,18 density: 0.58,19 seed: 99,20 }),21 [],22 );2324 const comparisonData = useMemo<HeatmapDay[]>(() => {25 return full.map((d) => ({26 date: d.date,27 value: Math.max(0, d.value + (d.date.endsWith("5") ? 2 : -1)),28 }));29 }, [full]);3031 const [visible, setVisible] = useState(0);3233 useEffect(() => {34 let raf = 0;35 const start = performance.now();36 const duration = 2200;37 const tick = (now: number) => {38 const t = Math.min(1, (now - start) / duration);39 setVisible(Math.floor(t * full.length));40 if (t < 1) raf = requestAnimationFrame(tick);41 };42 raf = requestAnimationFrame(tick);43 return () => cancelAnimationFrame(raf);44 }, [full.length]);4546 const data = useMemo(() => full.slice(0, visible), [full, visible]);47 const comparisonSlice = useMemo(48 () => comparisonData.slice(0, visible),49 [comparisonData, visible],50 );5152 return (53 <HeatmapCalendar54 data={data}55 year={DEMO_YEAR}56 weekStartsOn={1}57 comparisonData={comparisonSlice}58 showStreakSummary59 aria-label="Animated activity with comparison outline and streak summary"60 />61 );62}
Installation & source
Install via the shadcn CLI or copy the registry files manually.
bash
npx shadcn@latest add @tt-ui/heatmap-calendar
Props
| Name | Type | Default | Description |
|---|---|---|---|
| data | HeatmapDay[] | Required | Per-day values keyed by ISO `YYYY-MM-DD` dates |
| year / startDate + endDate | number | string | current year | Use `year` for Jan–Dec, or pass `startDate` and `endDate` for a custom range |
| weekStartsOn | 0 | 1 | 0 (Sunday) | First row weekday: Sunday or Monday |
| levels / getLevel / colors | number, fn, string[] | 5 levels + empty; GitHub-like greens via cssVars | Bucket count, optional custom bucketing, optional `levels + 1` fill colors |
| selectionMode / selectedRange / onSelectRange | "none" | "range" | "none" | Shift+click range; Enter twice from keyboard anchors a span |
| comparisonData | HeatmapDay[] | undefined | Optional second series drawn as a subtle outline on active days |
| showStreakSummary / renderSummary | boolean, fn | false | Footer streak stats or custom summary render |
| minCellSize / maxCellSize | number | 10 / 16 | Responsive cell sizing within the scrollable chart |