# React GitHub Contribution Graph: Data, Themes and A11y

> Add a react github contribution graph from a username or your own data, plus an activity heatmap with keyboard access and color scales that work in both themes.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Oct 19, 2026
- Category: [Component guides](https://wingo-ui.com/blog/category/components)
- Reading time: 9 min
- Canonical: https://wingo-ui.com/blog/react-github-contribution-graph

## TL;DR

For a portfolio or profile page, the free Wingo UI GitHub Activity renders a React GitHub contribution graph from a username or from data you fetch on the server, and fits its weeks to the screen so it never scrolls sideways. For your own product data (orders, workouts, CRM activity), the Pro Heatmap Calendar adds keyboard navigation, a spoken label per day, a screen reader table and a one-hue scale that stays readable in light and dark mode.

You want a React GitHub contribution graph on a portfolio, an "about" page or a developer profile: the familiar grid of green squares, one per day, with the yearly total on top. Or you want the same shape for your own data, like orders per day, team activity in a CRM or workout streaks. The first case is mostly about where the data comes from. The second is about scales, labels and keyboard access, because a grid of colored squares tells a screen reader nothing. This guide covers both with two Wingo UI components: the free [GitHub Activity](https://wingo-ui.com/components/github-activity) and the Pro [Heatmap Calendar](https://wingo-ui.com/components/heatmap-calendar).

## Which React contribution graph packages exist?

Three established npm packages draw this grid: react-activity-calendar, react-github-calendar and react-calendar-heatmap, all MIT licensed. Versions are from the npm registry as of October 19, 2026.

| Package | Version | What it does | Notes from its README |
|---|---|---|---|
| [react-activity-calendar](https://github.com/grubersjoe/react-activity-calendar) | 3.2.1 | Draws the grid from `{ date, count, level }` data | Any number of levels (including negative ones), color themes, dark and light mode, tooltips, localization, SSR supported |
| [react-github-calendar](https://github.com/grubersjoe/react-github-calendar) | 5.0.9 | Takes a `username` and fetches the calendar | Fetches in the browser, so the README says it does not render on the server; it points to react-activity-calendar for SSR |
| [react-calendar-heatmap](https://github.com/kevinsqi/react-calendar-heatmap) | 1.10.0 | SVG heatmap from `{ date, count }` | You map values to CSS classes with `classForValue` and style them yourself; tooltips through `titleForValue` or data attributes |

None of the three READMEs mentions keyboard navigation or screen reader output (checked October 19, 2026). If you want a packaged widget and can live with that, react-activity-calendar is the most complete. If you want source you can edit, with the same props, theming and motion as the rest of your UI, the two components below install into `components/ui`.

## How do I add a GitHub contribution graph React component?

Install GitHub Activity with the CLI and pass it a GitHub username:

```bash
npx wingo-ui@latest add github-activity
```

```tsx
import GitHubActivity from "@/components/ui/github-activity";

export function Profile() {
  return <GitHubActivity username="octocat" showMonths />;
}
```

With a `username`, the component loads two things in the browser. The calendar comes from [github-contributions-api](https://github.com/grubersjoe/github-contributions-api), a free service that reads the public profile page; its README says results are cached for one hour and uncached requests are limited to 10 per 10 seconds per IP. The top repositories in the footer come from GitHub's public events endpoint, which only returns [events from the past 30 days and at most 300 of them](https://docs.github.com/en/rest/activity/events), and unauthenticated REST calls are limited to [60 requests per hour](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api). Fine for a personal page; for anything with traffic, fetch on the server (next section).

An invalid or unknown login shows "No public activity found for {login}" (the `notFoundLabel` template, with the login filled in) and calls `onError`, so you can fall back to stored data.

The demo below uses the sample year that ships with the docs, so no request goes to GitHub.

> Live demo (GitHub Activity): Hover a square, or press one on a touchscreen, to read its count and date. Click the chevron to expand the top repositories over the grid, then click it again to fold them back. Try it at [GitHub Activity](https://wingo-ui.com/components/github-activity) and install it with `npx wingo-ui@latest add github-activity`.

## How do I render the contribution graph on the server?

Fetch the calendar from GitHub's GraphQL API in a server component and pass it as `contributions`. When you pass the data and no `username`, nothing is fetched in the browser, the token stays on the server, and the first HTML already has the right total. The [GraphQL reference](https://docs.github.com/en/graphql/reference/users) describes `contributionCalendar.weeks[].contributionDays[]` with a `date`, a `contributionCount` and a `contributionLevel` from `NONE` to `FOURTH_QUARTILE`; without `from` and `to`, the collection covers the last year.

```tsx
import GitHubActivity, { type Contribution, type ContributionLevel } from "@/components/ui/github-activity";

const LEVELS: Record<string, ContributionLevel> = {
  NONE: 0,
  FIRST_QUARTILE: 1,
  SECOND_QUARTILE: 2,
  THIRD_QUARTILE: 3,
  FOURTH_QUARTILE: 4,
};

const QUERY = `query ($login: String!) {
  user(login: $login) {
    contributionsCollection {
      contributionCalendar {
        weeks { contributionDays { date contributionCount contributionLevel } }
      }
    }
  }
}`;

type Day = { date: string; contributionCount: number; contributionLevel: string };

async function getContributions(login: string): Promise<Contribution[]> {
  const res = await fetch("https://api.github.com/graphql", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ query: QUERY, variables: { login } }),
    cache: "force-cache",
    next: { revalidate: 3600 },
  });
  if (!res.ok) return [];

  const json = await res.json();
  const weeks: { contributionDays: Day[] }[] =
    json.data?.user?.contributionsCollection.contributionCalendar.weeks ?? [];
  const days = weeks.flatMap((week) => week.contributionDays);
  // one column per week, so the data has to start on a sunday
  const start = days.findIndex((day) => new Date(`${day.date}T00:00:00Z`).getUTCDay() === 0);

  return days.slice(Math.max(0, start)).map((day) => ({
    date: day.date,
    count: day.contributionCount,
    level: LEVELS[day.contributionLevel] ?? 0,
  }));
}

export async function Contributions() {
  const contributions = await getContributions("your-login");
  if (contributions.length === 0) return null;

  return <GitHubActivity contributions={contributions} yearLabel="in the last year" showMonths className="max-w-2xl" />;
}
```

Two details. Caching is opt-in in Next.js 16, and `cache: "force-cache"` is what caches a POST that sends an authorization header ([fetch docs](https://nextjs.org/docs/app/api-reference/functions/fetch)); `revalidate: 3600` caps the cached copy at one hour. And the heading reads "{count} contributions in {year}" by default, with the year of the newest day; for a rolling window, `yearLabel="in the last year"` is more accurate. Pass `repos` as `{ name, count, href?, logo? }[]` if you want the footer panel too.

## How do I theme the contribution colors for light and dark mode?

Pass the colors through `accent`, as one color or an array of shades. One color (`#39d353` by default) is drawn at 30%, 52%, 76% and 100% opacity over an empty cell that is the foreground at 8%, so the same value works on the white card in light mode and the black card in dark mode. An array of four shades sets each level exactly, and five shades also replace the empty color:

```tsx
<GitHubActivity contributions={contributions} accent={["#0e4429", "#006d32", "#26a641", "#39d353"]} />
```

Those four greens are tuned for a dark background. If your shades differ per theme, pass CSS variables and define them for each theme in your stylesheet, for example `accent={["var(--commit-1)", "var(--commit-2)", "var(--commit-3)", "var(--commit-4)"]}`. The [Tailwind v4 dark mode guide](https://wingo-ui.com/blog/tailwind-v4-dark-mode) shows where those variables go. `background`, `radius`, `cellSize` (11px), `cellRadius` (3px) and `chevronColor` cover the rest of the look.

On a phone, the grid shows as many recent weeks as fit the card's width and drops older ones, so it never scrolls sideways. `months` caps the history, and a `max-w-*` class shows fewer weeks on purpose.

## When do I need a general activity heatmap in React?

As soon as the data is yours. A GitHub graph assumes the levels are already computed and the reader only needs a feel for the year. A dashboard tile with orders, invoices or support tickets per day needs more: your own thresholds, your own nouns in the labels, a selected day that drives a list, and real numbers for people who cannot see the colors. That is the job of an activity heatmap react component like the [Heatmap Calendar](https://wingo-ui.com/components/heatmap-calendar).

```bash
npx wingo-ui@latest add heatmap-calendar
```

```tsx
"use client";

import { useState } from "react";
import { HeatmapCalendar, type HeatmapDatum } from "@/components/ui/heatmap-calendar";

export function WorkoutHeatmap({ data }: { data: HeatmapDatum[] }) {
  const [day, setDay] = useState<string | null>(null);

  return (
    <HeatmapCalendar
      data={data}
      weeks={26}
      tone="success"
      thresholds={[1, 20, 40, 60]}
      value={day}
      onValueChange={setDay}
      locale="en-US"
      label="Workout minutes per day"
      labels={{
        total: (n, weeks) => `${n} minutes in the last ${weeks} weeks`,
        cell: (value, date) => `${value} minutes on ${date}`,
        empty: (date) => `No workout on ${date}`,
        valueName: "Minutes",
        table: "Workout minutes per month",
      }}
    />
  );
}
```

`data` is one `{ date, value }` per ISO day; missing days count as 0 and duplicate dates add up. Without `thresholds`, the levels come from quantiles of the non-zero values, so any activity is at least level 1. `endDate` defaults to the newest date in your data, never the clock, so the server and the browser draw the same grid. `weekStartsOn` follows the locale (Sunday in `en-US`, Monday in `en-GB`) unless you set it.

> Live demo (Heatmap calendar): Hover a day for its readout, click it to select it, then Tab into the grid and move with the arrow keys. Switch between All, Calls, Emails and Meetings to see the colors crossfade. Try it at [Heatmap calendar](https://wingo-ui.com/components/heatmap-calendar) and install it with `npx wingo-ui@latest add heatmap-calendar`.

## How do I make a react calendar heatmap accessible?

Make every number available without color. WCAG success criterion [1.4.1 Use of Color](https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html) asks that color is not the only way information is conveyed, and a heatmap is exactly that by default. In 2018 Adrian Roselli [reviewed GitHub's contributions chart](https://adrianroselli.com/2018/02/github-contributions-chart.html), found it unusable by keyboard and invisible to screen readers, and argued the data belongs in a table. GitHub [fixed its own graph in March 2023](https://github.blog/changelog/2023-03-02-accessibility-improvements-for-the-contribution-graph/): arrow keys move between days, and a screen reader hears the graph as a table and a summary of each day.

The Heatmap Calendar follows the same model:

- **One Tab stop.** Tab lands on the selected day or the newest one. Left and Right move by a week, Up and Down by a day, Home and End jump to the first and last day, PageUp and PageDown move four weeks, Enter or Space selects, Escape clears.
- **A spoken label per day** from `labels.cell` and `labels.empty`, such as "42 minutes on Thursday, September 24, 2026" in `en-US`, with the date written out for your locale.
- **A screen reader table** with each month's total and active days, captioned by `label`. `description` adds a one-sentence takeaway.
- **A scale that reads in both themes.** Level 0 is the foreground at 7%, visible on white and on the dark surface. The other levels are one hue from 30% to 100% in equal steps, so the order is carried by lightness. Set `tone`, or any CSS color with `color`, and `emptyColor` for the empty days.

GitHub Activity is lighter here. Its grid is one image named after the heading ("1234 contributions in the last year"), and the per-day tooltip answers hover and touch, not the keyboard. That suits a decorative profile card. When the daily numbers matter, use the Heatmap Calendar.

## How does the heatmap behave on a phone?

On a phone, the Heatmap Calendar either scrolls the year as a horizontal rail or shows only the weeks that fit, depending on `overflow`. `overflow="scroll"` (the default) turns the year into a horizontal rail that opens at the newest week, snaps by week, fades at the edges and keeps the weekday labels pinned; vertical drags still scroll the page. `overflow="fit"` shows only the newest weeks that fit, with nothing to scroll.

Touch has its own rules: the first tap on a day pins its readout above the grid (so a finger never covers it), a second tap on the same day selects it, and a tap outside clears it. `tooltipMode` defaults to `"auto"`, which uses that fixed readout on touch screens and a floating one next to the day elsewhere. The intro sweep and the color crossfade respect reduced motion, as explained in the [prefers-reduced-motion guide](https://wingo-ui.com/blog/prefers-reduced-motion-react).

## What are the common contribution graph pitfalls?

These four break a contribution graph in ways that are easy to miss in development:

- **Data that does not start on a Sunday.** GitHub Activity draws one column per seven entries of the `contributions` you pass, and only trims its own fetched data. Start the array on a Sunday or every column shears. The server example above trims the first partial week.
- **Calling GitHub from every visitor's browser.** The free APIs have per-IP limits and the events endpoint only covers 30 days. Fetch on the server and cache it.
- **Reading "today" during render.** The Heatmap Calendar reads the viewer's date only after hydration for the today ring and uses your data for `endDate`. Keep it that way in your own wrappers, or the server and client grids disagree.
- **A heatmap as the only view of a KPI.** Pair it with a number: a [Stat Card](https://wingo-ui.com/components/stat-card) for the total or a [Sparkline](https://wingo-ui.com/components/sparkline) for the trend. The [dashboard library comparison](https://wingo-ui.com/blog/best-react-component-library-for-dashboard) covers the rest of a dashboard.

## How do I add these to my project?

Run `npx wingo-ui@latest add github-activity` for the free contribution graph, or `npx wingo-ui@latest add heatmap-calendar` for the general heatmap (Pro, see [pricing](https://wingo-ui.com/pricing)). The CLI copies the source into `components/ui` and adds what it imports. More single-component deep dives live in the [component guides category](https://wingo-ui.com/blog/category/components), and the [React form components guide](https://wingo-ui.com/blog/react-form-components-guide) applies the same keyboard and screen reader rules to every field.

## Components in this post

- [GitHub Activity](https://wingo-ui.com/components/github-activity) (Free): A React GitHub contribution graph: a yearly heatmap with a panel of your top repositories. Install: `npx wingo-ui@latest add github-activity`
- [Heatmap calendar](https://wingo-ui.com/components/heatmap-calendar) (Pro): A React heatmap calendar of activity by day over weeks or a year, on a one-hue scale with month and weekday labels, a total and a legend. Install: `npx wingo-ui@latest add heatmap-calendar`

## FAQ

### How do I show my GitHub contribution graph in React?

Install a component that draws the yearly grid and give it either a username or the daily data. With Wingo UI, <GitHubActivity username="your-login" /> loads the last year in the browser, or you fetch the calendar on the server with the GitHub GraphQL API and pass it as contributions.

### Where does contribution graph data come from?

GitHub's GraphQL API exposes it as user.contributionsCollection.contributionCalendar, with a count and a level for each day; it needs a token. Without a token, react-github-calendar and the Wingo UI component's username mode call the free github-contributions-api, which reads the public profile page and caches results for one hour.

### What is the difference between a contribution graph and a calendar heatmap?

A contribution graph is a calendar heatmap with GitHub's data and look: weeks as columns, days as rows, five shades of green. A general react calendar heatmap takes any daily value, such as orders or minutes, and lets you choose the scale, the colors and the labels.

### Is a heatmap calendar accessible?

Only if the numbers are available without color. Give each day a text label, make the grid reachable by keyboard, and offer a table or a summary for screen readers, which is what GitHub added to its own graph in March 2023.

### Is the Wingo UI GitHub Activity free?

Yes. GitHub Activity is in the free tier and installs with npx wingo-ui@latest add github-activity. The Heatmap Calendar is a Pro component.

---

Source: https://wingo-ui.com/blog/react-github-contribution-graph
