npm.io
0.2.0 • Published 9h ago

github-contribution-svg

Licence
MIT
Version
0.2.0
Deps
0
Size
52 kB
Vulns
0
Weekly
0
Stars
1

github-contribution-svg

npm npm downloads CI zero dependencies

A GitHub contribution calendar as a crisp inline SVG for React. Zero dependencies, themeable with plain CSS custom properties, SSR-friendly, and happy to render data you fetched yourself.

Live on adnanreza.com in both light and dark themes.

Light Dark
Light theme Dark theme

Why another one?

react-github-calendar is excellent and more featureful — use it if you want labels, legends, and options. This package is the minimalist take: one component, no dependencies, no shipped stylesheet, cells styled by CSS variables so the chart inherits your design tokens (including dark mode) with no theme objects or filters.

Install

npm install github-contribution-svg

React ≥ 18 is a peer dependency.

Quick start

import { ContributionGraph } from "github-contribution-svg";

// Convenience mode: fetches the last year client-side (no token needed)
<ContributionGraph username="octocat" />

The SVG scales to its container (it only sets a viewBox), so size it with CSS. Until data arrives it renders null — reserve space with aspect-ratio if you want zero layout shift: 52 weeks at default geometry is 673 / 88.

Bring your own data (SSR / one fetch for chart + total)

import { ContributionGraph, fetchContributions } from "github-contribution-svg";

const { contributions, totalLastYear } = await fetchContributions("octocat");

<figure>
  <ContributionGraph data={contributions} />
  <figcaption>{totalLastYear} contributions in the last year</figcaption>
</figure>

data mode makes no network request, so it works in server components, static builds, or with data from your own proxy.

Prefer the convenience of username mode but still want the total for a caption? Use onData (and onError to degrade gracefully):

const [total, setTotal] = useState<number | null>(null);

<ContributionGraph
  username="octocat"
  skeleton
  onData={({ totalLastYear }) => setTotal(totalLastYear)}
  onError={() => setTotal(null)}
/>
{total != null && <p>{total} contributions in the last year</p>}

Theming

Cells fill from var(--gcs-cell-{level}, <github-green default>) and carry the classes gcs-cell gcs-l0 … gcs-l4. Define five custom properties and the chart is yours:

.my-chart {
  --gcs-cell-0: oklch(90% 0.008 240);   /* no contributions */
  --gcs-cell-1: oklch(80% 0.055 245);
  --gcs-cell-2: oklch(68% 0.095 245);
  --gcs-cell-3: oklch(55% 0.125 245);
  --gcs-cell-4: oklch(41% 0.13 245);    /* most contributions */
}
[data-theme="dark"] .my-chart {
  --gcs-cell-0: oklch(26% 0 0);
  --gcs-cell-4: oklch(79% 0.1 245);     /* brightest = most active */
  /* ... */
}

No JavaScript theme objects, no filter: invert() hacks — dark mode is just different variables.

Responsive recipe

A full year squeezed into a phone screen means ~4px cells. Render two instances and let a media query pick one:

<ContributionGraph data={days} weeks={52} className="chart chart--full" />
<ContributionGraph data={days} weeks={26} className="chart chart--half" />
.chart--half { display: none; }
@media (max-width: 640px) {
  .chart--full { display: none; }
  .chart--half { display: block; }
}

Use data mode for both instances (as above) so the community API is hit once, not twice. If you started in username mode, capture the payload from one instance with onData and feed the second via data.

Loading skeleton

Pass skeleton and the component renders a ghost grid (same geometry, all cells at level 0, aria-hidden) while there's nothing to draw — during the username-mode fetch, or in data mode while your own request is in flight. It appears in server-rendered HTML, so SSR users get it at first paint. If the username-mode fetch fails, the skeleton is removed rather than pulsing forever.

<ContributionGraph username="octocat" skeleton />

It's static by default (no CSS ships with this package). One optional rule adds a pulse:

@media (prefers-reduced-motion: no-preference) {
  .gcs-skeleton { animation: gcs-pulse 1.8s ease-in-out infinite; }
}
@keyframes gcs-pulse { 50% { opacity: 0.55; } }
CSS-only variant (shows before your JS bundle loads)

For a skeleton visible even before hydration, paint a CSS-only ghost grid on the container and drop it when loaded:

.chart-frame.is-loading {
  background-color: var(--gcs-cell-0, #ebedf0);
  /* one tile = a cell (10/13 of the pitch) + a gap stripe; 52 columns */
  background-image:
    repeating-linear-gradient(to right,
      transparent 0 calc(100% / 52 * 10 / 13),
      var(--page-bg, #fff) calc(100% / 52 * 10 / 13) calc(100% / 52)),
    repeating-linear-gradient(to bottom,
      transparent 0 calc(100% / 7 * 10 / 13),
      var(--page-bg, #fff) calc(100% / 7 * 10 / 13) calc(100% / 7));
}
@media (prefers-reduced-motion: no-preference) {
  .chart-frame.is-loading { animation: pulse 1.8s ease-in-out infinite; }
}
@keyframes pulse { 50% { opacity: 0.55; } }

Toggle is-loading off when your data lands (and on fetch failure, so it doesn't shimmer forever). Server-render the class for the first-paint benefit.

Props

Prop Default Description
data ContributionDay[] ({date, count, level}), date-ascending. Disables fetching.
username GitHub username; fetches the last year client-side.
url jogruber.de API Endpoint template for username mode; {username} is substituted.
skeleton false Ghost grid while no data is available; removed on fetch failure.
onData Username mode: (payload) => void after a successful fetch ({contributions, totalLastYear}).
onError Username mode: (error) => void when the fetch fails.
weeks 52 Trailing weeks to render.
cellSize / gap / radius 10 / 3 / 2 Geometry in SVG units.
tooltips true Native per-cell <title> tooltips.
ariaLabel derived Accessible label for the role="img" SVG.
className Class on the <svg>.

Also exported: buildWeeks(days) (Sunday-start week bucketing) and fetchContributions(username, {url, signal}).

Data source & credits

Username mode uses the free community github-contributions-api by @grubersjoe, which reads the public profile calendar — please be considerate with request volume, and point url at a self-hosted instance for anything high-traffic.

License

MIT Adnan Reza

Keywords