Search

Search the site

All components

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.
JanFebMarAprMayJunJulAugSepOctNovDec
1"use client";
2
3import { useMemo } from "react";
4import {
5 HeatmapCalendar,
6 generateHeatmapDemoData,
7} from "@/registry/ui/heatmap-calendar";
8
9const DEMO_YEAR = 2026;
10
11export 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 );
22
23 return (
24 <HeatmapCalendar
25 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.
JanFebMarAprMayJunJulAugSepOctNovDec

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.

1"use client";
2
3import { useMemo, useState } from "react";
4import {
5 HeatmapCalendar,
6 generateHeatmapDemoData,
7 type HeatmapDateRange,
8} from "@/registry/ui/heatmap-calendar";
9
10const DEMO_YEAR = 2026;
11
12export 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 );
23
24 const [range, setRange] = useState<HeatmapDateRange | null>({
25 start: `${DEMO_YEAR}-03-01`,
26 end: `${DEMO_YEAR}-03-14`,
27 });
28
29 return (
30 <div className="w-full min-w-0 max-w-full space-y-2">
31 <HeatmapCalendar
32 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 {range
43 ? `${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 stay
47 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
JanFebMarAprMayJunJulAugSepOctNovDec
1"use client";
2
3import { useEffect, useMemo, useState } from "react";
4import {
5 HeatmapCalendar,
6 generateHeatmapDemoData,
7 type HeatmapDay,
8} from "@/registry/ui/heatmap-calendar";
9
10const DEMO_YEAR = 2026;
11
12export 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 );
23
24 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]);
30
31 const [visible, setVisible] = useState(0);
32
33 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]);
45
46 const data = useMemo(() => full.slice(0, visible), [full, visible]);
47 const comparisonSlice = useMemo(
48 () => comparisonData.slice(0, visible),
49 [comparisonData, visible],
50 );
51
52 return (
53 <HeatmapCalendar
54 data={data}
55 year={DEMO_YEAR}
56 weekStartsOn={1}
57 comparisonData={comparisonSlice}
58 showStreakSummary
59 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

NameTypeDefaultDescription
dataHeatmapDay[]RequiredPer-day values keyed by ISO `YYYY-MM-DD` dates
year / startDate + endDatenumber | stringcurrent yearUse `year` for Jan–Dec, or pass `startDate` and `endDate` for a custom range
weekStartsOn0 | 10 (Sunday)First row weekday: Sunday or Monday
levels / getLevel / colorsnumber, fn, string[]5 levels + empty; GitHub-like greens via cssVarsBucket 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
comparisonDataHeatmapDay[]undefinedOptional second series drawn as a subtle outline on active days
showStreakSummary / renderSummaryboolean, fnfalseFooter streak stats or custom summary render
minCellSize / maxCellSizenumber10 / 16Responsive cell sizing within the scrollable chart