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// GitHub contribution graph replica
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);
const totalContributions = sampleData.reduce((sum, d) => sum + d.count, 0);
export default function DemoGitHub() {
return (
<div className="w-full max-w-4xl p-6 space-y-2">
{/* GitHub header style */}
<div className="text-sm text-muted-foreground">
<span className="font-semibold text-foreground">
{totalContributions.toLocaleString()} contributions
</span>{" "}
in the last year
</div>
{/* GitHub exact colors */}
<div
style={
{
// GitHub's exact green colors (light mode)
"--contribution-empty": "#ebedf0",
"--contribution-level-1": "#9be9a8",
"--contribution-level-2": "#40c463",
"--contribution-level-3": "#30a14e",
"--contribution-level-4": "#216e39",
} as React.CSSProperties
}
className="dark:hidden"
>
<ContributionGraph
data={sampleData}
startDate={startDate.toISOString().split("T")[0]}
endDate={endDate.toISOString().split("T")[0]}
cellSize={10}
cellRadius={2}
gap={3}
formatCount={(count) => {
if (count === 0) return "No contributions";
return `${count} contribution${count !== 1 ? "s" : ""}`;
}}
formatDate={(date) => {
return new Date(date).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
}}
>
<ContributionGraphGrid />
<ContributionGraphTooltip />
<div className="mt-2 flex items-center justify-between text-xs text-muted-foreground">
<a href="#" className="hover:text-primary">
Learn how we count contributions
</a>
<ContributionGraphLegend lessLabel="Less" moreLabel="More" />
</div>
</ContributionGraph>
</div>
{/* GitHub dark mode colors */}
<div
style={
{
// GitHub's exact green colors (dark mode)
"--contribution-empty": "#161b22",
"--contribution-level-1": "#0e4429",
"--contribution-level-2": "#006d32",
"--contribution-level-3": "#26a641",
"--contribution-level-4": "#39d353",
} as React.CSSProperties
}
className="hidden dark:block"
>
<ContributionGraph
data={sampleData}
startDate={startDate.toISOString().split("T")[0]}
endDate={endDate.toISOString().split("T")[0]}
cellSize={10}
cellRadius={2}
gap={3}
formatCount={(count) => {
if (count === 0) return "No contributions";
return `${count} contribution${count !== 1 ? "s" : ""}`;
}}
formatDate={(date) => {
return new Date(date).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
}}
>
<ContributionGraphGrid />
<ContributionGraphTooltip />
<div className="mt-2 flex items-center justify-between text-xs text-muted-foreground">
<a href="#" className="hover:text-primary">
Learn how we count contributions
</a>
<ContributionGraphLegend lessLabel="Less" moreLabel="More" />
</div>
</ContributionGraph>
</div>
</div>
);
}