GitHub-style contribution calendar that turns a year of activity into a wall of green — pass a username and it fetches, caches, and animates the data itself.
npx shadcn@latest add @iconiq/contribution-graphimport {
ContributionGraph,
ContributionGraphBlock,
ContributionGraphCalendar,
ContributionGraphFooter,
ContributionGraphLegend,
ContributionGraphTotalCount,
} from "@/components/ui/contribution-graph";
export function GitHubActivity() {
return (
<ContributionGraph username="edwinvakayil">
<ContributionGraphCalendar>
{({ activity, dayIndex, weekIndex }) => (
<ContributionGraphBlock
activity={activity}
dayIndex={dayIndex}
weekIndex={weekIndex}
/>
)}
</ContributionGraphCalendar>
<ContributionGraphFooter>
<ContributionGraphTotalCount />
<ContributionGraphLegend />
</ContributionGraphFooter>
</ContributionGraph>
);
}The username prop fetches on the client and keeps an in-memory cache per username. If you would rather fetch once on the server — and cache across visitors — fetch the same API inside unstable_cache in a server component and pass the result through data and totalCount instead.
import { unstable_cache } from "next/cache";
import {
type Activity,
ContributionGraph,
ContributionGraphBlock,
ContributionGraphCalendar,
ContributionGraphFooter,
ContributionGraphLegend,
ContributionGraphTotalCount,
} from "@/components/ui/contribution-graph";
type Response = {
total: Record<string, number>;
contributions: Activity[];
};
const username = "edwinvakayil";
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 }
);
export async function GitHubActivity() {
const { contributions, total } = await getCachedContributions();
return (
<ContributionGraph data={contributions} totalCount={total}>
<ContributionGraphCalendar>
{({ activity, dayIndex, weekIndex }) => (
<ContributionGraphBlock
activity={activity}
dayIndex={dayIndex}
weekIndex={weekIndex}
/>
)}
</ContributionGraphCalendar>
<ContributionGraphFooter>
<ContributionGraphTotalCount />
<ContributionGraphLegend />
</ContributionGraphFooter>
</ContributionGraph>
);
}dataRaw activities to render — objects with an ISO date, a count, and a level between 0 and maxLevel. Missing days inside the range are filled with level 0. When provided, data wins over username and no fetch happens.
Type Activity[]
usernameGitHub username to fetch contributions for from github-contributions-api.jogruber.de (rolling last year). While loading, the grid renders as a shimmering skeleton; on failure it collapses to a quiet inline error. Responses are cached in memory per username, so remounts replay the entrance without refetching.
Type string
animatedPlay the level-by-level reveal: the muted grid fades in as a canvas, then blocks light up in rounds from lightest to darkest green with a springy pop. Set to false to render the finished grid immediately.
Type boolean·Default true
blockSizeSide length of one day block, in pixels.
Type number·Default 12
blockMarginGap between blocks, in pixels.
Type number·Default 4
blockRadiusCorner radius of each block, in pixels.
Type number·Default 2
fontSizeFont size for month labels and footer text, in pixels.
Type number·Default 14
labelsOverrides for month names, weekday names, the total-count template ({{count}} and {{year}} placeholders), and the legend's Less/More text.
Type Labels
maxLevelHighest activity level in the data. Blocks outside 0..maxLevel throw a RangeError.
Type number·Default 4
totalCountExplicit total for ContributionGraphTotalCount. Defaults to the fetched yearly total for username data, or the sum of counts otherwise.
Type number
weekStartWhich weekday starts a column (0 is Sunday).
Type 0 | 1 | 2 | 3 | 4 | 5 | 6·Default 0
childrenRender prop for one day cell — usually a ContributionGraphBlock forwarded the same three arguments.
Type ({ activity, dayIndex, weekIndex }) => ReactNode
hideMonthLabelsDrop the month labels row above the grid.
Type boolean·Default false
classNameExtra classes for the scroll container. The grid scrolls horizontally when wider than its parent.
Type string
activityThe day to render, as passed by the calendar render prop.
Type Activity
weekIndexColumn index, used for x position and the block's reveal jitter.
Type number
dayIndexRow index, used for y position and the block's reveal jitter.
Type number
showTooltipShows the styled commit count and date tooltip on hover or keyboard focus.
Type boolean·Default true
classNameExtra classes for the rect. The default green ramp targets data-level, so you can swap the palette with your own data-[level] fills.
Type string
childrenRender prop replacing the default formatted text.
Type ({ totalCount, year }) => ReactNode
childrenRender prop replacing the default swatch for each level.
Type ({ level }) => ReactNode
Layout row under the calendar for the total count and legend. Plain flex container — style it with className.
Install the exact registry entry shown on the right when you want the component file and its declared runtime dependencies together.
Dependencies: date-fns.
Contact
Additionally, if you find any bug or issue, feel free to raise an issue.