Components
Loading preview...
A composable, customizable contribution graph component inspired by GitHub's activity heatmap. Features composable subcomponents (Grid, Tooltip, Legend), CSS variable theming, configurable cell sizes, interactive tooltips with custom render functions, and full dark mode support.
npx shadcn@latest add https://21st.dev/r/bankkroll/contribution-graph// Custom tooltip demo
import {
ContributionGraph,
ContributionGraphGrid,
ContributionGraphTooltip,
ContributionGraphLegend,
type ContributionData,
} from "@/components/ui/contribution-graph";
function generateSampleData(): ContributionData[] {
const data: ContributionData[] = [];
const endDate = new Date();
const startDate = new Date();
startDate.setFullYear(startDate.getFullYear() - 1);
const current = new Date(startDate);
while (current <= endDate) {
const dayOfWeek = current.getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
const baseChance = isWeekend ? 0.3 : 0.6;
if (Math.random() < baseChance) {
data.push({
date: current.toISOString().split("T")[0],
count: Math.floor(Math.random() * 12) + 1,
});
}
current.setDate(current.getDate() + 1);
}
return data;
}
const sampleData = generateSampleData();
const endDate = new Date();
const startDate = new Date();
startDate.setFullYear(startDate.getFullYear() - 1);
startDate.setDate(startDate.getDate() + 1);
// Calculate stats for tooltip
const totalContributions = sampleData.reduce((sum, d) => sum + d.count, 0);
const avgPerDay = (totalContributions / 365).toFixed(1);
function getActivityLevel(count: number): { label: string; color: string } {
if (count === 0) return { label: "No activity", color: "text-muted-foreground" };
if (count <= 2) return { label: "Light", color: "text-blue-500" };
if (count <= 5) return { label: "Moderate", color: "text-green-500" };
if (count <= 8) return { label: "Active", color: "text-orange-500" };
return { label: "Very Active", color: "text-red-500" };
}
function getRelativeDate(dateStr: string): string {
const date = new Date(dateStr);
const now = new Date();
const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
if (diffDays === 0) return "Today";
if (diffDays === 1) return "Yesterday";
if (diffDays < 7) return `${diffDays} days ago`;
if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`;
if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`;
return `${Math.floor(diffDays / 365)} years ago`;
}
export default function DemoCustomTooltip() {
return (
<div className="w-full max-w-4xl p-6">
<ContributionGraph
data={sampleData}
startDate={startDate.toISOString().split("T")[0]}
endDate={endDate.toISOString().split("T")[0]}
>
<ContributionGraphGrid />
<ContributionGraphTooltip className="min-w-[180px] p-0 overflow-hidden">
{({ date, count }) => {
const activity = getActivityLevel(count);
const dayOfWeek = new Date(date).toLocaleDateString("en-US", { weekday: "long" });
return (
<div className="divide-y divide-border">
{/* Header */}
<div className="px-3 py-2 bg-muted/50">
<div className="flex items-center justify-between gap-3">
<span className="text-[10px] uppercase tracking-wider text-muted-foreground">
{dayOfWeek}
</span>
<span className={`text-[10px] font-medium ${activity.color}`}>
{activity.label}
</span>
</div>
<div className="text-xs text-muted-foreground mt-0.5">
{getRelativeDate(date)}
</div>
</div>
{/* Stats */}
<div className="px-3 py-2 space-y-1.5">
<div className="flex items-baseline justify-between">
<span className="text-2xl font-bold text-popover-foreground tabular-nums">
{count}
</span>
<span className="text-[10px] text-muted-foreground">
contribution{count !== 1 ? "s" : ""}
</span>
</div>
{/* Progress bar */}
<div className="space-y-1">
<div className="h-1.5 w-full bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all duration-300"
style={{ width: `${Math.min(100, (count / 12) * 100)}%` }}
/>
</div>
<div className="flex justify-between text-[9px] text-muted-foreground">
<span>0</span>
<span>Daily avg: {avgPerDay}</span>
<span>12+</span>
</div>
</div>
</div>
{/* Footer */}
<div className="px-3 py-1.5 bg-muted/30">
<div className="text-[10px] text-muted-foreground">
{new Date(date).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})}
</div>
</div>
</div>
);
}}
</ContributionGraphTooltip>
<ContributionGraphLegend className="mt-3" />
</ContributionGraph>
</div>
);
}