How I Built an Animated GitHub Profile README (ASCII Portrait + Neofetch Card + Live Contribution Graph)
Overview
My GitHub profile reads like a terminal: a monochrome ASCII portrait that "types" itself in, a neofetch-style info card next to it, and — above both — a contribution heatmap that reveals itself box by box and refreshes with real data every day. No profile-stats third-party services, no GitHub token, no JavaScript. Everything is animated SVG generated by a handful of Python scripts and kept fresh by a GitHub Actions cron.
The one constraint that shapes everything: GitHub
strips <script> from READMEs and sanitizes almost
all inline CSS — but it does render SVGs embedded via
<img> and runs their SMIL / CSS-keyframe
animations. So the trick to an "animated" README is to push all the
motion into self-contained SVG files and let the README just place
them.
What you'll build
-
avi-ascii.svg— a photo turned into clean, one-color ASCII art that prints row by row. -
info-card.svg— a neofetch-style panel of role, stack, and highlights that fades in line by line. -
contrib-heatmap.svg— your real 53-week contribution calendar, rendered as rounded boxes that slide in diagonally. -
A
README.mdthat arranges them in a terminal layout. - A GitHub Actions workflow that regenerates the heatmap daily and commits it.
Step 1 — Create the magic repo
GitHub gives every account one special repository: a repo whose name is
exactly your username. Its README.md renders at
the top of your profile page.
Step 2 — Set up the Python toolchain
The portrait pipeline needs image libraries; the heatmap needs an HTTP
client and an HTML parser. Create scripts/requirements.txt:
Then python -m venv .venv && source .venv/bin/activate
&& pip install -r scripts/requirements.txt. The
portrait libraries only run locally when you change your photo — the
daily automation just needs requests and
beautifulsoup4.
Step 3 — Turn a photo into ASCII art
This is two scripts on purpose: prep the photo once, then convert it to an SVG.
3a. Prep the photo (prep_photo.py)
A flatly-lit face converts to a dark, unreadable blob. Three steps fix that:
-
Remove the background with
rembgso the subject is isolated. - Boost local contrast with OpenCV's CLAHE (contrast-limited adaptive histogram equalization) — this is what gives a flat face real highlights and shadows.
- Composite onto pure white so the background maps to the blank end of the ASCII ramp (white → spaces).
Output is a grayscale source-prepped.png. Run it once per
photo:
3b. Convert to a self-typing SVG (make_ascii_svg.py)
The prepped image is downsampled to a character grid (~100×53), and each pixel's brightness picks a glyph from a density ramp — sparse characters for bright areas, dense ones for dark:
Two design choices make it look clean instead of noisy:
- Monochrome. One light-gray fill color. Per-character rainbow coloring is exactly what makes most ASCII portraits look like static.
- High contrast. A busy background washes out to the space glyph, so only the subject prints.
For the animation, each row is wrapped in a horizontal clip that wipes left-to-right (a small block "cursor" rides the wipe edge), staggered top to bottom. The whole portrait prints once and freezes — no looping. Because it's SMIL inside the SVG, GitHub plays it.
Step 4 — Build the neofetch info card
make_info_card.py hand-authors a small SVG that looks like
the output of the neofetch command: a title bar, then
colored key/value rows — Now, Prev,
Stack, Highlights. Keep the content here and
not in the contribution graph; the graph already covers your
GitHub stats, so the card is for the story numbers can't tell.
Each line fades and slides in on a short stagger so the panel looks
like it's printing next to the portrait. A STATIC=1 env
var emits a frozen frame for local Quick Look previews.
Step 5 — Render the live contribution heatmap
This is the part that stays alive, and it's two scripts again.
5a. Get real data — no token (fetch_contributions.py)
You don't need the GraphQL API or a personal access token. GitHub serves
your contribution calendar as public HTML at
https://github.com/users/<username>/contributions —
the same fragment the profile page itself uses. Fetch it with
requests, parse the day cells with BeautifulSoup, and write
data/contributions.json with raw days plus derived stats
(current streak, longest streak, best day, monthly totals).
5b. Draw the grid (render_heatmap_svg.py)
Render the JSON as the classic 53-week × 7-day calendar of rounded, colored boxes using a GitHub-ish green ramp:
Reveal it once with a diagonal, line-after-line slide-down (CSS
keyframes that play on load, then freeze — no looping "glow"), and add
a Less→More legend plus a stats footer ("9,376 contributions in the last year"). Output: contrib-heatmap.svg.
Step 6 — Compose the README
Now the README just places the three SVGs in a centered terminal
layout. The portrait and card sit side by side in a
<table> (the only reliable way to put two images on
one row on GitHub), each column top-aligned. I label sections with
fake shell prompts to sell the terminal feel:
Keep the widths aligned: the heatmap's 860 equals the two
columns (370 + 490), so the edges line up cleanly.
GitHub markdown gotchas that cost me time:
-
Inline
styleis stripped. Astyle="margin-top:36px"does nothing. The only vertical spacing GitHub honors is<br>tags. -
<h1>and<h2>draw a full-width underline rule. Great as a divider, distracting as a title. Use<h3>when you don't want the line. - No JavaScript, and external CSS is blocked — the animation must live entirely inside each SVG.
Step 7 — Auto-refresh it daily with GitHub Actions
The portrait and info card are static (regenerate them only when your
photo or details change). Only the heatmap needs to update, so a small
workflow re-scrapes and re-renders it on a cron and commits the result.
Create .github/workflows/update-profile-art.yml:
The [skip ci] keeps the bot's own commit from re-triggering
the workflow, and contents: write lets it push back to the
repo. Trigger it once by hand from the Actions tab
(workflow_dispatch) to confirm it commits a fresh SVG.
Why SVG instead of a stats service
Plenty of README widgets exist as hosted images, but they render on someone else's server, rate-limit, and occasionally go down with a broken-image icon on your profile. Generating your own SVGs means the art is committed to your repo, loads instantly, looks exactly how you designed it, and the only moving part you depend on is a public GitHub HTML endpoint that needs no auth.
Recap
- Make the
<username>/<username>repo. - Prep a photo → convert to a self-typing monochrome ASCII SVG.
- Hand-author a neofetch-style info card SVG.
- Scrape your public contributions → render an animated heatmap SVG.
- Lay the three out in a centered, terminal-styled README.
- Cron a GitHub Action to refresh the heatmap every day.
The full source — every script and the workflow — lives in my profile repo at github.com/AVIVASHISHTA29.