Collaboration
Project Management
Finance
Contribution Graph
A GitHub-style contribution graph component that displays activity levels over time.
Installation
npx love-ui@latest add contribution-graphUsage
import { ContributionGraph, ContributionGraphCalendar, ContributionGraphBlock, ContributionGraphFooter, ContributionGraphTotalCount, ContributionGraphLegend } from "@/components/contribution-graph"<ContributionGraph>
<ContributionGraphCalendar data={contributions}>
{(activity) => <ContributionGraphBlock key={activity.date} activity={activity} />}
</ContributionGraphCalendar>
<ContributionGraphFooter>
<ContributionGraphTotalCount total={totalContributions} />
<ContributionGraphLegend />
</ContributionGraphFooter>
</ContributionGraph>Features
- GitHub-style activity calendar visualization
- Fully composable architecture with render props
- CSS-based theming with data attributes
- Configurable block size, margin, and radius
- Tooltip support with render prop
- Month and week labels
- Activity count and legend
- Responsive design with horizontal scrolling
- Server-side data fetching support
- TypeScript support
Data Fetching
This component is the visualization layer only, it doesn't handle data fetching or state management.
I highly recommend using Jonathan Gruber's GitHub Contributions API to fetch your data.
Here's an example of how to fetch and cache your data:
const username = 'loveconnor';
const getCachedContributions = unstable_cache(
async () => {
const url = new URL(`/v4/${username}`, 'https://github-contributions-api.jogruber.de');
const response = await fetch(url);
const data = (await response.json()) as Response;
const total = data.total[new Date().getFullYear()];
return { contributions: data.contributions, total };
},
['github-contributions'],
{ revalidate: 60 * 60 * 24 },
);Examples
Custom GitHub theme
Use GitHub's color scheme with data attributes:
"use client";
import {
ContributionGraph,
ContributionGraphBlock,
ContributionGraphCalendar,
ContributionGraphFooter,
} from "../../../../../packages/contribution-graph";
import { eachDayOfInterval, endOfYear, formatISO, startOfYear } from "date-fns";
import { cn } from "@/lib/utils";
const maxCount = 20;
const maxLevel = 4;
const now = new Date();
const days = eachDayOfInterval({
start: startOfYear(now),
end: endOfYear(now),
});
const data = days.map((date) => {
const c = Math.round(
Math.random() * maxCount - Math.random() * (0.8 * maxCount)
);
const count = Math.max(0, c);
const level = Math.ceil((count / maxCount) * maxLevel);
return {
date: formatISO(date, { representation: "date" }),
count,
level,
};
});
const Example = () => (
<ContributionGraph data={data}>
<ContributionGraphCalendar>
{({ activity, dayIndex, weekIndex }) => (
<ContributionGraphBlock
activity={activity}
className={cn(
'data-[level="0"]:fill-[#ebedf0] dark:data-[level="0"]:fill-[#161b22]',
'data-[level="1"]:fill-[#9be9a8] dark:data-[level="1"]:fill-[#0e4429]',
'data-[level="2"]:fill-[#40c463] dark:data-[level="2"]:fill-[#006d32]',
'data-[level="3"]:fill-[#30a14e] dark:data-[level="3"]:fill-[#26a641]',
'data-[level="4"]:fill-[#216e39] dark:data-[level="4"]:fill-[#39d353]'
)}
dayIndex={dayIndex}
weekIndex={weekIndex}
/>
)}
</ContributionGraphCalendar>
<ContributionGraphFooter />
</ContributionGraph>
);
export default Example;
Minimal view
Hide labels and footer for a compact display:
"use client";
import {
ContributionGraph,
ContributionGraphBlock,
ContributionGraphCalendar,
} from "../../../../../packages/contribution-graph";
import { eachDayOfInterval, endOfYear, formatISO, startOfYear } from "date-fns";
const maxCount = 20;
const maxLevel = 4;
const now = new Date();
const days = eachDayOfInterval({
start: startOfYear(now),
end: endOfYear(now),
});
const data = days.map((date) => {
const c = Math.round(
Math.random() * maxCount - Math.random() * (0.8 * maxCount)
);
const count = Math.max(0, c);
const level = Math.ceil((count / maxCount) * maxLevel);
return {
date: formatISO(date, { representation: "date" }),
count,
level,
};
});
const Example = () => (
<ContributionGraph data={data}>
<ContributionGraphCalendar hideMonthLabels>
{({ activity, dayIndex, weekIndex }) => (
<ContributionGraphBlock
activity={activity}
dayIndex={dayIndex}
weekIndex={weekIndex}
/>
)}
</ContributionGraphCalendar>
</ContributionGraph>
);
export default Example;
With tooltips
Add interactive tooltips to show detailed information:
"use client";
import {
ContributionGraph,
ContributionGraphBlock,
ContributionGraphCalendar,
ContributionGraphFooter,
} from "../../../../../packages/contribution-graph";
import { eachDayOfInterval, endOfYear, formatISO, startOfYear } from "date-fns";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
const maxCount = 20;
const maxLevel = 4;
const now = new Date();
const days = eachDayOfInterval({
start: startOfYear(now),
end: endOfYear(now),
});
const data = days.map((date) => {
const c = Math.round(
Math.random() * maxCount - Math.random() * (0.8 * maxCount)
);
const count = Math.max(0, c);
const level = Math.ceil((count / maxCount) * maxLevel);
return {
date: formatISO(date, { representation: "date" }),
count,
level,
};
});
const Example = () => (
<TooltipProvider>
<ContributionGraph data={data}>
<ContributionGraphCalendar>
{({ activity, dayIndex, weekIndex }) => (
<Tooltip>
<TooltipTrigger
render={
<ContributionGraphBlock
activity={activity}
className="cursor-pointer"
dayIndex={dayIndex}
weekIndex={weekIndex}
/>
}
/>
<TooltipContent>
<p className="font-semibold">{activity.date}</p>
<p>{activity.count} contributions</p>
</TooltipContent>
</Tooltip>
)}
</ContributionGraphCalendar>
<ContributionGraphFooter />
</ContributionGraph>
</TooltipProvider>
);
export default Example;
Custom size
Adjust block size and spacing:
1392 activities in 2025
LessMore
"use client";
import {
ContributionGraph,
ContributionGraphBlock,
ContributionGraphCalendar,
ContributionGraphFooter,
ContributionGraphLegend,
ContributionGraphTotalCount,
} from "../../../../../packages/contribution-graph";
import { eachDayOfInterval, endOfYear, formatISO, startOfYear } from "date-fns";
const maxCount = 20;
const maxLevel = 4;
const now = new Date();
const days = eachDayOfInterval({
start: startOfYear(now),
end: endOfYear(now),
});
const data = days.map((date) => {
const c = Math.round(
Math.random() * maxCount - Math.random() * (0.8 * maxCount)
);
const count = Math.max(0, c);
const level = Math.ceil((count / maxCount) * maxLevel);
return {
date: formatISO(date, { representation: "date" }),
count,
level,
};
});
const Example = () => (
<ContributionGraph blockMargin={2} blockSize={20} data={data} fontSize={16}>
<ContributionGraphCalendar>
{({ activity, dayIndex, weekIndex }) => (
<ContributionGraphBlock
activity={activity}
dayIndex={dayIndex}
weekIndex={weekIndex}
/>
)}
</ContributionGraphCalendar>
<ContributionGraphFooter>
<ContributionGraphTotalCount />
<ContributionGraphLegend />
</ContributionGraphFooter>
</ContributionGraph>
);
export default Example;
Custom block styling
Customize individual blocks with className and style props:
1462 activities in 2025
LessMore
"use client";
import {
ContributionGraph,
ContributionGraphBlock,
ContributionGraphCalendar,
ContributionGraphFooter,
ContributionGraphLegend,
ContributionGraphTotalCount,
} from "../../../../../packages/contribution-graph";
import { eachDayOfInterval, endOfYear, formatISO, startOfYear } from "date-fns";
const maxCount = 20;
const maxLevel = 4;
const now = new Date();
const days = eachDayOfInterval({
start: startOfYear(now),
end: endOfYear(now),
});
const data = days.map((date) => {
const c = Math.round(
Math.random() * maxCount - Math.random() * (0.8 * maxCount)
);
const count = Math.max(0, c);
const level = Math.ceil((count / maxCount) * maxLevel);
return {
date: formatISO(date, { representation: "date" }),
count,
level,
};
});
const Example = () => (
<ContributionGraph data={data}>
<ContributionGraphCalendar>
{({ activity, dayIndex, weekIndex }) => (
<ContributionGraphBlock
activity={activity}
className={
activity.level > 3
? "animate-pulse stroke-2 stroke-emerald-500 dark:stroke-emerald-400"
: activity.level === 0
? "opacity-50"
: ""
}
dayIndex={dayIndex}
style={{
filter: activity.level > 2 ? "brightness(1.2)" : undefined,
}}
weekIndex={weekIndex}
/>
)}
</ContributionGraphCalendar>
<ContributionGraphFooter>
<ContributionGraphTotalCount />
<ContributionGraphLegend />
</ContributionGraphFooter>
</ContributionGraph>
);
export default Example;
Custom footer
Create a custom footer with composable components:
Year 2025:1,493 contributions
Less
Level 0
Level 1
Level 2
Level 3
Level 4
More"use client";
import {
ContributionGraph,
ContributionGraphBlock,
ContributionGraphCalendar,
ContributionGraphFooter,
ContributionGraphLegend,
ContributionGraphTotalCount,
} from "../../../../../packages/contribution-graph";
import { eachDayOfInterval, endOfYear, formatISO, startOfYear } from "date-fns";
import { Badge } from "@/components/ui/badge";
const maxCount = 20;
const maxLevel = 4;
const now = new Date();
const days = eachDayOfInterval({
start: startOfYear(now),
end: endOfYear(now),
});
const data = days.map((date) => {
const c = Math.round(
Math.random() * maxCount - Math.random() * (0.8 * maxCount)
);
const count = Math.max(0, c);
const level = Math.ceil((count / maxCount) * maxLevel);
return {
date: formatISO(date, { representation: "date" }),
count,
level,
};
});
const Example = () => (
<ContributionGraph data={data}>
<ContributionGraphCalendar>
{({ activity, dayIndex, weekIndex }) => (
<ContributionGraphBlock
activity={activity}
dayIndex={dayIndex}
weekIndex={weekIndex}
/>
)}
</ContributionGraphCalendar>
<ContributionGraphFooter>
<ContributionGraphTotalCount>
{({ totalCount, year }) => (
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-sm">Year {year}:</span>
<Badge variant="secondary">
{totalCount.toLocaleString()} contributions
</Badge>
</div>
)}
</ContributionGraphTotalCount>
<ContributionGraphLegend>
{({ level }) => (
<div
className="group relative flex h-3 w-3 items-center justify-center"
data-level={level}
>
<div
className={`h-full w-full rounded-sm border border-border ${level === 0 ? "bg-muted" : ""} ${level === 1 ? "bg-emerald-200 dark:bg-emerald-900" : ""} ${level === 2 ? "bg-emerald-400 dark:bg-emerald-700" : ""} ${level === 3 ? "bg-emerald-600 dark:bg-emerald-500" : ""} ${level === 4 ? "bg-emerald-800 dark:bg-emerald-300" : ""} `}
/>
<span className="-top-8 absolute hidden rounded bg-popover px-2 py-1 text-popover-foreground text-xs shadow-md group-hover:block">
Level {level}
</span>
</div>
)}
</ContributionGraphLegend>
</ContributionGraphFooter>
</ContributionGraph>
);
export default Example;
Usage Tips
- For missing dates in the data array, no activity is assumed
- The calendar will automatically fill gaps between dates with zero activity
- You can control the calendar's date range by including empty entries as the first and last items
- The component automatically handles dark mode when using the system color scheme