Here's an uncomfortable truth: your GitHub profile README is the most-viewed file you'll ever write, and almost nobody reads it. It's the first thing that loads on your profile — and it's usually a pile of badges, a streak counter, and a "most used languages" card that looks exactly like everyone else's. Wallpaper. You scroll past your own.
I snapped one evening and decided mine would do something. Not look busy — do something. Two weeks later my profile is a mission console orbiting a living planet, and every panel on it is a real instrument wired to my actual data. It rebuilds itself every morning while I'm asleep.
This is the whole teardown — the one weird constraint that makes this genuinely hard, the SVG trick that beats it, the maths behind the rings, and the three bugs that stole my weekend. Steal all of it.

The hero. The rings are my contribution calendar; the readout is live.
The rule I made before writing any code
I wrote one sentence on a sticky note:
If it moves, it has to mean something.
That rule is the entire difference between a toy and a dashboard, and it was brutal. It killed half my ideas on the spot. No glowing lines for vibes. No waveform that's just a pretty sine. Every animated pixel has to be a real number — my commits, my pull requests, my languages, my writing — or it doesn't ship.
Hold that thought, because I break it later and have to fix it.
First, the problem nobody warns you about
I sat down to animate my profile and immediately hit a wall:
GitHub READMEs can't run JavaScript. None. No <script>, no <canvas>, no WebGL, no click handlers. GitHub strips all of it for security. You get Markdown and images. That's the whole sandbox.
So how is anything on my profile moving?
Here's the trick, and it's the most useful thing in this post: an SVG animates itself even when it's just an <img>. GitHub strips scripts, but it keeps declarative animation — plain CSS @keyframes and SMIL baked into the SVG. Drop this in a README:
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="60">
<style>
.dot { animation: pulse 1.8s ease-in-out infinite; }
@keyframes pulse { 50% { opacity: .2; } }
</style>
<circle class="dot" cx="30" cy="30" r="8" fill="#5bd6ff"/>
</svg>
…reference it as , and that dot breathes. Forever. Inside a file GitHub swore couldn't run code.
That one loophole is the foundation of everything below. Every spinning ring, radar sweep, and flickering readout is just an SVG pretending to be a picture.
Now let's build the console.
The planet wears my contribution graph as rings
The hero is a slowly rotating planet, and the rings around it aren't decoration — they're my contribution calendar bent into orbit.
Seven rings, one per weekday. Each ring is split into 53 segments, one per week. A segment lights up — cyan, then mint, then gold — based on how hard I committed that day. A lazy month shows as thin, dim inner arcs. A sprint lights up a bright band around the equator.
The trick that makes it cheap is a one-line SVG superpower: pathLength. I tell each ring "your circumference is 53 units long," so now one unit = one week, and a dash pattern can paint activity week by week:
// each ring is one weekday; 53 segments = 53 weeks of the year
<ellipse pathLength="53"
stroke-dasharray="0 12 0.7 0.3 0 40" /* gap, dash(week), gap... */
... />
A slow drift animation spins the whole system so it feels alive:
.ring { animation: ringdrift 90s linear infinite; }
@keyframes ringdrift { to { stroke-dashoffset: -53; } }
One last touch sells the 3D: I clip the rings into a front half and a back half and draw them on either side of the planet, so they pass behind it at the top and in front at the bottom. Without that, it's a sticker. With it, the planet sits inside its own orbit.
A radar, a spectrum, and the signal I got wrong
Three more instruments, three more promises kept to that sticky note.
The radar is my open-source footprint — the pull requests I've opened in other people's repos, the work that never shows up in your own repo list. Each blip is a real PR. The dish sweeps, the blips ping as it passes.
The spectrum turns my languages into emission lines, like you're reading the light off a star. Fat cyan line? TypeScript. Thin green sliver? Shell. The widths are the real percentages, not a vibe.
The signal is the one I'm proudest of — because I first built it wrong. For about ten minutes it was a gorgeous sine wave… that meant absolutely nothing. It broke the rule, so I ripped it out and plotted what it should have been all along: my reactions per post on dev.to.
Now the tallest spike on the oscilloscope is my most-reacted article. The sweep "intercepts" each post in turn. It reads like a transmission log and it's 100% my real engagement. The second I swapped the fake wave for real numbers, the panel went from screensaver to dashboard. That's the whole thesis in one commit.

The signal: every point is a post, the spike is my most-reacted one.
Ten files pretending to be one screen
The profile looks like a single console, but it's actually ten separate SVGs stacked in the README — hero, link badges, missions, radar, spectrum, signal, footer:





Why split them? One giant SVG is miserable to maintain, and every panel is a different height. The catch: stacked images show hairline seams and the illusion cracks. The fix is shared chrome — every panel draws the same corner brackets, the same bezel, and the same scanline sweeping down it, so ten images read as one continuous screen:
.scan { animation: scan 8s linear infinite; }
@keyframes scan { from { transform: translateY(-90px); } to { transform: translateY(100%); } }
Consistency is the trick that turns a stack of pictures into one machine.

One of the ten panels — flagship repos as mission dossiers, each with its own orbiting moon.
It files its own report every morning
Here's the part that still feels like magic to me: I never touch it.
A scheduled GitHub Action runs a small Node script every day, pulls fresh data, regenerates all ten SVGs, and commits them if anything changed:
on:
schedule: [{ cron: '17 3 * * *' }] # 03:17, not 03:00 — see below
workflow_dispatch:
(Why :17 and not :00? Because every cron job on Earth fires on the hour and GitHub's schedulers get jammed. Seventeen past is quiet, so it actually runs on time. Learned that the annoying way.)
The data comes from two places. GitHub's GraphQL API hands me the entire contribution calendar in one call, and I map its intensity levels straight onto the ring colors:
const LEVEL = { NONE:0, FIRST_QUARTILE:1, SECOND_QUARTILE:2, THIRD_QUARTILE:3, FOURTH_QUARTILE:4 };
weeks[w][day] = LEVEL[contributionLevel]; // → which ring segment lights, and how bright
The writing signal comes from dev.to's public API — no key required, so the Action needs no extra secret:
const posts = await (await fetch(`https://dev.to/api/articles?username=${me}&per_page=30`)).json();
const signal = posts.slice(0, 16).map(p => p.public_reactions_count).reverse(); // the oscilloscope
Three habits that keep it boringly reliable: it fails soft (an API hiccup keeps yesterday's numbers instead of shipping a blank console), it commits only on a real change (no noisy history), and the token is a fine-grained, read-only PAT living in Actions secrets, never the repo.
Three bugs that ate my weekend
Every project has the bugs that teach you something. Here are mine, worst to dumbest.
1. The 7 MB planet that wouldn't spin. I spent a giddy hour baking a 72-frame rotating globe into the hero SVG… then learned that when GitHub renders an SVG as an image, a nested animation freezes on frame one — and the file had bloated to 7 MB. Lesson: declarative SVG animation survives the image sandbox; a video stuffed inside it does not. So the globe is a crisp still, and the rings do the spinning. (The actually-rotating planet lives in the standalone web version, where real motion is allowed.)
2. The bars that refused to exist. A row of stat bars just… didn't render. No error. Empty tracks. The cause? Each bar was a <span>, and inline elements ignore width and height. One display:block and every bar snapped into place. Ten years in, and CSS still finds new ways to make me feel like it's day one.
3. The fonts that lied. Everything looked perfect on my machine and slightly wrong on everyone else's — a title wrapping one character early, kerning off. Silent system-font fallback. The fix: subset the font to only the ~80 glyphs I actually use and embed it as base64, so the type is pixel-identical everywhere and still tiny.
So… was it worth it?
Honest damage report: ten hand-built SVG generators, a ring-projection system, a font-subsetting step, a two-source data pipeline, and a daily robot — for a file people glance at for four seconds.
Worth it. Here's why. A profile that just looks cool is a one-time hit — screenshot, move on. But a profile where the rings are my calendar, the radar is my open-source trail, and the signal is my readership is something sneakier: a status board for a career. It quietly rewards me for shipping and it's honest when I don't. That accountability turned out to be the real feature — and it made me care about my profile again, which is the small true thing under all the sci-fi.
Build your own
The whole generator is open and made to drop into any profile repo — change your username in one JSON file, copy the scripts and the Action, add the markers to your README, run it once from the Actions tab. After that it flies itself.
It's live on my profile right now. Go watch it turn:
If you wire up your own instrument panel, tag me — I really want to see which numbers you choose to put on the dials. Follow along here for more "turn your dev data into something alive" teardowns. 👋
One question for you: if you could put a single number from your dev life on a dial the whole world could see — commits, PRs, reactions, review time, cups of coffee — which one would you pick? And be honest: would making it visible actually make you ship more? 👇


Top comments (0)