GitHub keeps a contribution calendar for user accounts and nothing else. A repository doesn't have one, and neither does an organization. That's why every contribution snake you've seen sits on somebody's profile page.
I wanted the snake on two pages that don't qualify: the README of stellar-odyssey, and the profile of the FailRouter organization. So I wrote github-action-shares, a GitHub Action that builds the calendar itself and draws three animated SVGs from it. It took two days, from v1.0.0 to v1.2.0.
What lands in your README
One run writes eight SVGs (a light and a dark version of each) and a stats.json:
| File | What it shows |
|---|---|
snake.svg |
a snake that eats the year, nearest square first |
heatmap.svg |
the familiar grid, filling in week by week |
skyline.svg |
a 3D isometric skyline with the busiest day and the longest streak |
activity.svg |
the snake, or a one-line summary while the project is young |
stats.json |
total, busiest day, streaks, first and last active day |
The heatmap and the skyline are drawn from the same year as the snake at the top of this post:
All three draw the same daily numbers. Pick one per page. Three charts of one dataset make a reader scroll past the same year three times.
A young repo doesn't show a year of empty squares
This was the first thing that looked wrong. A repository that is two months old, drawn on GitHub's 53-week grid, is forty-five weeks of grey followed by a few green columns. To a visitor that reads as "barely touched", which is the opposite of what a project activity chart is for.
window: auto starts the grid at the week of the first commit, at least eight weeks wide. Stellar Odyssey started on 2026-07-19, so its grid is twelve weeks, not fifty-three:
Below ten active days even a cropped grid is mostly empty, so activity.svg turns into a sentence. The FailRouter org page shows that state right now:
On its tenth active day the same URL starts serving the snake. Nobody edits the README for that, and no bot commits to it either.
Set it up for a repository in three steps
Add the workflow. Save this as .github/workflows/readme-showcase.yml:
name: README showcase
on:
schedule: [{ cron: "23 3 * * *" }]
workflow_dispatch:
permissions:
contents: write
jobs:
art:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with: { fetch-depth: 0 } # git log needs the full history
- uses: HowardZlh/github-action-shares@v1
with: { source: repo, git-path: ., window: auto, out-dir: dist }
- uses: crazy-max/ghaction-github-pages@v5
with: { target_branch: output, build_dir: dist, keep_history: false }
env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" }
Run it once. Open the Actions tab, pick the workflow, and press Run workflow. After that the nightly schedule takes over.
The run takes 10 to 15 seconds and creates an output branch with the images:
Point your README at it. Replace OWNER/REPO:
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/OWNER/REPO/output/activity-dark.svg">
<img alt="OWNER/REPO commits since the first one" src="https://raw.githubusercontent.com/OWNER/REPO/output/activity.svg">
</picture>
<picture> picks the dark file for visitors on GitHub's dark theme. Write a real alt; screen readers read it and image search indexes it.
Your profile and your organization
On a profile, source: user reads your contribution calendar through GraphQL, so private work counts too if you've ticked Private contributions in your profile settings. The profile example pairs the snake with github-profile-3d-contrib for the 3D calendar, and adds three shields.io badges that read their numbers from stats.json. Here is my profile in dark mode:
For an organization, source: org sums the commit statistics of every public, non-fork repository. The same action has a second command, readme, that rewrites two marked blocks in a README: a project table sorted by stars and a recent-activity list. On an org page that list includes lines like "⭐ someone starred your-repo", which is social proof a personal profile can't show.
You can copy both setups from examples/, and the tutorial walks through each one with the same screenshots.
How it works
Three sources, one calendar. A repository is counted from git log dates in the checkout. An organization is the sum of /repos/{repo}/stats/commit_activity across its public repos. A user comes from the GraphQL contributionCalendar, the same numbers GitHub draws on the profile. Everything after that step only sees a list of { date, count }.
Animation without JavaScript. GitHub serves README images through its camo proxy as plain <img> tags. Scripts never run there, and hover events never fire. What does work is CSS @keyframes and SMIL. The heatmap and the skyline use keyframes with a per-cell delay. The snake is a <rect> per body segment moving along an <animateMotion> path, and each square it eats has its own <animate> that turns it grey at the exact step the head arrives.
The snake takes the short way. It enters at the top left, always heads for the nearest uneaten green square one cell at a time, and leaves through the nearer edge. The step length is chosen so a loop lasts between 8 and 30 seconds, whether the year had five active days or three hundred.
My first version swept all 371 cells in order. On a sparse year it spent ten seconds crawling over grey before it reached anything green.
Nothing lands on your default branch. The images go to an output branch through crazy-max/ghaction-github-pages with keep_history: false, so that branch is always one commit. If merging to main deploys your site, a nightly art refresh never triggers it.
Quartile colours, like GitHub. The four greens are cut at the quartiles of your non-zero days, not at a fraction of the maximum. One 68-commit day doesn't wash the rest of the year out to the palest shade.
Respectful defaults. Under prefers-reduced-motion: reduce the SVGs show the final frame. Each one has role="img", a <title> with the real number and a <desc> with the date range. A longest streak under three days is left out of the text, because "longest streak: 1 day" helps nobody.
Zero dependencies. It's a composite action that runs node bin/showcase.mjs: about 850 lines of JavaScript using the built-in fetch and string templates. There's no node_modules to audit and no Docker image to pull. Tests use node:test against a throwaway git repository and a fake fetch, and CI fails under 90% line coverage.
Tools it stands on
| Tool | Job |
|---|---|
| GitHub Actions, composite action | runs the generator on a schedule |
Node.js built-ins (fetch, node:test) |
API calls, tests, coverage, no packages |
| GitHub REST and GraphQL APIs | commit statistics, contribution calendar, stars, events |
| crazy-max/ghaction-github-pages | pushes the SVGs to the output branch |
| raw.githubusercontent.com | serves the images to your README |
| shields.io dynamic JSON badges | live numbers from stats.json
|
| github-profile-3d-contrib (optional) | the 3D calendar on a profile |
Things I ran into
-
Not every badge host gets through camo. Visitor-counter images from komarev.com load in a browser and return
404 Cannot proxy the given URLinside a README. Copy the image address from the rendered page andcurlit before you rely on a badge. -
The org's own
.githubrepo broke the org chart. Every commit to the profile README made GitHub recompute that repo's statistics, and the API kept answering202 Accepted. The action now skips.githuband skips a repo whose statistics are still cold with a warning instead of failing the run. -
Pull request events lost their titles. Since 2025 the Events API ships pull request events without
titleorhtml_url, so the activity list fetches/pulls/{n}for the few it shows.
For comparison, here is how it differs from the snake most profiles use:
| Platane/snk | github-action-shares | |
|---|---|---|
| Works for | users | users, repos, orgs |
| Eating order | one colour level after another | nearest square first |
| Loop on a busy year I measured | about 85 s | 8 to 30 s by design |
Limits
README images can't be interactive. If you want tooltips or zoom, link the image to a page you host. The org chart only sees public repositories, and it inherits GitHub's 52-week window for commit statistics. GitHub pauses scheduled workflows in a public repository after 60 days without activity, so a dormant repo's chart stops refreshing until you re-enable it.
Try it
Copy the workflow above into one of your repos, press Run workflow, and paste the five lines of <picture> into the README. The whole thing takes about two minutes, most of it waiting for the first run.
HowardZlh
/
github-action-shares
Animated contribution heatmap, snake and 3D skyline SVGs for any GitHub repo, org or user, plus auto-updated stars and activity in your README. Zero-dependency GitHub Action, MIT.
Animated GitHub README stats for any user, organization or repository
English | 中文
A GitHub Action that draws a contribution heatmap, a snake that eats it, and a 3D skyline, as animated SVGs, for a single repository or a whole organization as well as for a user account. It also keeps a star-sorted project table and a recent-activity list in your README up to date. No dependencies, MIT.
That snake is eating a year of @HowardZlh's contributions, redrawn every night by showcase.yml. A two-month-old repo gets a grid that starts at its first commit instead of a year of empty squares, and a one-line summary until it has ten active days.
Jump to: What you get · On real pages · Quick start · Profile · Organization · Inputs · Tutorial · How it works
What you get
Every SVG comes in a light and a dark…
If you put it on a page, open a PR that adds your repo to the Used by list. And if it saved you an evening, a star on the repo helps the next person find it.









Top comments (0)