Calendar Heatmap
Month-by-month calendar heatmap with customizable color variants and weighted date support
March 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
April 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
May 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Installation
Dependencies
Examples
Circle
October 2025
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 | 1 |
November 2025
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 26 | 27 | 28 | 29 | 30 | 31 | 1 |
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 | 1 | 2 | 3 | 4 | 5 | 6 |
December 2025
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 30 | 1 | 2 | 3 | 4 | 5 | 6 |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 | 1 | 2 | 3 |
January 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 28 | 29 | 30 | 31 | 1 | 2 | 3 |
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
February 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
March 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 | 1 | 2 | 3 | 4 |
"use client";
import { CalendarHeatmap } from "@/components/vritti/calendar-heatmap";
import type { WeightedDateEntry } from "@/components/vritti/calendar-heatmap";
function randomDate(start: Date, end: Date): Date {
return new Date(
start.getTime() + Math.random() * (end.getTime() - start.getTime())
);
}
const OceanBlue = [
"text-white hover:text-white bg-sky-200 hover:bg-sky-200",
"text-white hover:text-white bg-sky-400 hover:bg-sky-400",
"text-white hover:text-white bg-blue-500 hover:bg-blue-500",
"text-white hover:text-white bg-blue-700 hover:bg-blue-700",
"text-white hover:text-white bg-indigo-800 hover:bg-indigo-800",
];
const now = new Date();
const start = new Date(now.getFullYear(), now.getMonth() - 5, 1);
const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
const weightedDates: WeightedDateEntry[] = [...Array(80)].map(() => ({
date: randomDate(start, end),
weight: Math.floor(Math.random() * 100),
}));
export function CalendarHeatmapCircleExample() {
return (
<div className="flex items-center justify-center overflow-x-auto p-4">
<CalendarHeatmap
shape="circle"
numberOfMonths={6}
variantClassnames={OceanBlue}
weightedDates={weightedDates}
startMonth={start}
defaultMonth={start}
/>
</div>
);
}
Interactive
March 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
April 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Click on a highlighted date to see details
"use client";
import { useState } from "react";
import { format } from "date-fns";
import { CalendarHeatmap } from "@/components/vritti/calendar-heatmap";
import type { WeightedDateEntry } from "@/components/vritti/calendar-heatmap";
function randomDate(start: Date, end: Date): Date {
return new Date(
start.getTime() + Math.random() * (end.getTime() - start.getTime())
);
}
const now = new Date();
const start = new Date(now.getFullYear(), now.getMonth(), 1);
const end = new Date(now.getFullYear(), now.getMonth() + 2, 0);
const Heatmap = [
"text-white hover:text-white bg-blue-300 hover:bg-blue-300 cursor-pointer",
"text-white hover:text-white bg-blue-400 hover:bg-blue-400 cursor-pointer",
"text-white hover:text-white bg-blue-500 hover:bg-blue-500 cursor-pointer",
"text-white hover:text-white bg-blue-700 hover:bg-blue-700 cursor-pointer",
];
const weightedDates: WeightedDateEntry[] = [...Array(30)].map(() => ({
date: randomDate(start, end),
weight: Math.floor(Math.random() * 100) + 1,
}));
export function CalendarHeatmapInteractiveExample() {
const [selected, setSelected] = useState<{
date: string;
weight: number;
} | null>(null);
return (
<div className="flex flex-col items-center justify-center gap-4 overflow-x-auto p-4">
<CalendarHeatmap
numberOfMonths={2}
variantClassnames={Heatmap}
weightedDates={weightedDates}
tooltipContent={(date) => {
const entry = weightedDates.find(
(e) =>
format(e.date, "yyyy-MM-dd") === format(date, "yyyy-MM-dd")
);
return (
<div className="text-sm">
<p className="font-medium">{format(date, "MMM d, yyyy")}</p>
<p className="text-muted-foreground">
{entry?.weight ?? 0} events
</p>
</div>
);
}}
onDayClick={(date) => {
const dateKey = format(date, "yyyy-MM-dd");
const entry = weightedDates.find(
(e) => format(e.date, "yyyy-MM-dd") === dateKey
);
if (entry) {
setSelected({
date: format(date, "MMMM d, yyyy"),
weight: entry.weight,
});
} else {
setSelected(null);
}
}}
/>
{selected ? (
<div className="rounded-lg border bg-card p-3 text-sm shadow-sm">
<p className="font-medium">{selected.date}</p>
<p className="text-muted-foreground">{selected.weight} events recorded</p>
</div>
) : (
<p className="text-sm text-muted-foreground">
Click on a highlighted date to see details
</p>
)}
</div>
);
}
Multi Month
October 2025
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 | 1 |
November 2025
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 26 | 27 | 28 | 29 | 30 | 31 | 1 |
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 | 1 | 2 | 3 | 4 | 5 | 6 |
December 2025
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 30 | 1 | 2 | 3 | 4 | 5 | 6 |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 | 1 | 2 | 3 |
January 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 28 | 29 | 30 | 31 | 1 | 2 | 3 |
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
February 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
March 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 | 1 | 2 | 3 | 4 |
"use client";
import { CalendarHeatmap } from "@/components/vritti/calendar-heatmap";
import type { WeightedDateEntry } from "@/components/vritti/calendar-heatmap";
function randomDate(start: Date, end: Date): Date {
return new Date(
start.getTime() + Math.random() * (end.getTime() - start.getTime())
);
}
const Thermal = [
"text-white hover:text-white bg-sky-200 hover:bg-sky-200",
"text-white hover:text-white bg-sky-400 hover:bg-sky-400",
"text-white hover:text-white bg-amber-300 hover:bg-amber-300",
"text-white hover:text-white bg-orange-500 hover:bg-orange-500",
"text-white hover:text-white bg-red-600 hover:bg-red-600",
];
const now = new Date();
const start = new Date(now.getFullYear(), now.getMonth() - 5, 1);
const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
const weightedDates: WeightedDateEntry[] = [...Array(80)].map(() => ({
date: randomDate(start, end),
weight: Math.floor(Math.random() * 100),
}));
export function CalendarHeatmapMultiMonthExample() {
return (
<div className="flex items-center justify-center overflow-x-auto p-4">
<CalendarHeatmap
numberOfMonths={6}
variantClassnames={Thermal}
weightedDates={weightedDates}
startMonth={start}
defaultMonth={start}
/>
</div>
);
}
Rainbow
March 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 | 1 | 2 | 3 | 4 |
April 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 29 | 30 | 31 | 1 | 2 | 3 | 4 |
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 1 | 2 |
"use client";
import { CalendarHeatmap } from "@/components/vritti/calendar-heatmap";
function randomDate(start: Date, end: Date): Date {
return new Date(
start.getTime() + Math.random() * (end.getTime() - start.getTime())
);
}
const now = new Date();
const start = new Date(now.getFullYear(), now.getMonth(), 1);
const end = new Date(now.getFullYear(), now.getMonth() + 2, 0);
const Rainbow = [
"text-white hover:text-white bg-violet-400 hover:bg-violet-400",
"text-white hover:text-white bg-indigo-400 hover:bg-indigo-400",
"text-white hover:text-white bg-blue-400 hover:bg-blue-400",
"text-white hover:text-white bg-green-400 hover:bg-green-400",
"text-white hover:text-white bg-yellow-400 hover:bg-yellow-400",
"text-white hover:text-white bg-orange-400 hover:bg-orange-400",
"text-white hover:text-white bg-red-400 hover:bg-red-400",
];
const RainbowDates = Rainbow.map((_, i) =>
[...Array(i % 2 === 0 ? 3 : 2)].map(() => randomDate(start, end))
);
export function CalendarHeatmapRainbowExample() {
return (
<div className="flex items-center justify-center p-4">
<CalendarHeatmap
numberOfMonths={2}
variantClassnames={Rainbow}
datesPerVariant={RainbowDates}
/>
</div>
);
}
Tooltip
March 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
April 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
"use client";
import { format } from "date-fns";
import { CalendarHeatmap } from "@/components/vritti/calendar-heatmap";
import type { WeightedDateEntry } from "@/components/vritti/calendar-heatmap";
function randomDate(start: Date, end: Date): Date {
return new Date(
start.getTime() + Math.random() * (end.getTime() - start.getTime())
);
}
const now = new Date();
const start = new Date(now.getFullYear(), now.getMonth(), 1);
const end = new Date(now.getFullYear(), now.getMonth() + 2, 0);
const GithubStreak = [
"text-white hover:text-white bg-green-300 hover:bg-green-300",
"text-white hover:text-white bg-green-400 hover:bg-green-400",
"text-white hover:text-white bg-green-500 hover:bg-green-500",
"text-white hover:text-white bg-green-700 hover:bg-green-700",
];
const weightedDates: WeightedDateEntry[] = [...Array(30)].map(() => ({
date: randomDate(start, end),
weight: Math.floor(Math.random() * 20) + 1,
}));
export function CalendarHeatmapTooltipExample() {
return (
<div className="flex items-center justify-center overflow-x-auto p-4">
<CalendarHeatmap
numberOfMonths={2}
variantClassnames={GithubStreak}
weightedDates={weightedDates}
tooltipContent={(date) => (
<div className="text-sm">
<p className="font-medium">{format(date, "MMM d, yyyy")}</p>
<p className="text-muted-foreground">
{weightedDates.find(
(e) => format(e.date, "yyyy-MM-dd") === format(date, "yyyy-MM-dd")
)?.weight ?? 0}{" "}
contributions
</p>
</div>
)}
/>
</div>
);
}
Weighted
March 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 | 1 | 2 | 3 | 4 |
April 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 29 | 30 | 31 | 1 | 2 | 3 | 4 |
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 1 | 2 |
"use client";
import { CalendarHeatmap } from "@/components/vritti/calendar-heatmap";
import type { WeightedDateEntry } from "@/components/vritti/calendar-heatmap";
function randomDate(start: Date, end: Date): Date {
return new Date(
start.getTime() + Math.random() * (end.getTime() - start.getTime())
);
}
const Heatmap = [
"text-white hover:text-white bg-blue-300 hover:bg-blue-300",
"text-white hover:text-white bg-green-500 hover:bg-green-500",
"text-white hover:text-white bg-amber-400 hover:bg-amber-400",
"text-white hover:text-white bg-red-700 hover:bg-red-700",
];
const now = new Date();
const start = new Date(now.getFullYear(), now.getMonth(), 1);
const end = new Date(now.getFullYear(), now.getMonth() + 2, 0);
const weightedDates: WeightedDateEntry[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map(
(weight) => ({
date: randomDate(start, end),
weight,
})
);
export function CalendarHeatmapWeightedExample() {
return (
<div className="flex items-center justify-center p-4">
<CalendarHeatmap
numberOfMonths={2}
variantClassnames={Heatmap}
weightedDates={weightedDates}
/>
</div>
);
}