Contribution Graph

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.

Installation

npx shadcn@latest add @iconiq/contribution-graph

File Structure

Usage

import {
  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>
  );
}

Server-side data

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>
  );
}

Props

Props
Description

ContributionGraph

data

Raw 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[]

username

GitHub 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

animated

Play 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

blockSize

Side length of one day block, in pixels.

Type number·Default 12

blockMargin

Gap between blocks, in pixels.

Type number·Default 4

blockRadius

Corner radius of each block, in pixels.

Type number·Default 2

fontSize

Font size for month labels and footer text, in pixels.

Type number·Default 14

labels

Overrides for month names, weekday names, the total-count template ({{count}} and {{year}} placeholders), and the legend's Less/More text.

Type Labels

maxLevel

Highest activity level in the data. Blocks outside 0..maxLevel throw a RangeError.

Type number·Default 4

totalCount

Explicit total for ContributionGraphTotalCount. Defaults to the fetched yearly total for username data, or the sum of counts otherwise.

Type number

weekStart

Which weekday starts a column (0 is Sunday).

Type 0 | 1 | 2 | 3 | 4 | 5 | 6·Default 0

ContributionGraphCalendar

children

Render prop for one day cell — usually a ContributionGraphBlock forwarded the same three arguments.

Type ({ activity, dayIndex, weekIndex }) => ReactNode

hideMonthLabels

Drop the month labels row above the grid.

Type boolean·Default false

className

Extra classes for the scroll container. The grid scrolls horizontally when wider than its parent.

Type string

ContributionGraphBlock

activity

The day to render, as passed by the calendar render prop.

Type Activity

weekIndex

Column index, used for x position and the block's reveal jitter.

Type number

dayIndex

Row index, used for y position and the block's reveal jitter.

Type number

showTooltip

Shows the styled commit count and date tooltip on hover or keyboard focus.

Type boolean·Default true

className

Extra 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

ContributionGraphTotalCount

children

Render prop replacing the default formatted text.

Type ({ totalCount, year }) => ReactNode

ContributionGraphLegend

children

Render prop replacing the default swatch for each level.

Type ({ level }) => ReactNode

ContributionGraphFooter

Layout row under the calendar for the total count and legend. Plain flex container — style it with className.

Registry bundle

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.