GitHub's contribution graph is one long 53-week strip. It is fine for seeing a streak, but if you want to answer "what did I do on the 14th", you are counting squares sideways with your finger on the screen.

So I rewrote it as twelve small month grids, six per row, weekday names on top. Like a wall calendar. I opened a PR against Refined GitHub.

The maintainer closed it:

This is not a refinement, please publish as a standalone extension.

I think they were right, which is annoying, because I liked the feature. This is what I built, why the rejection was correct, and where it lives now.

Moving the cells, not redrawing them

The obvious way to build a calendar is to read the contribution data and draw new squares. That is the wrong way, because every square GitHub renders already carries a tooltip, a data-level attribute driving its color, and the theme variables that go with it.

So instead of cloning anything, I move the original <td> elements into new tables:

const months = new Map();
for (const cell of table.querySelectorAll('td.ContributionCalendar-day[data-date]')) {
	const [year, month, day] = cell.dataset.date.split('-').map(Number);
	const monthKey = `${year}-${month - 1}`;
	if (!months.has(monthKey)) {
		months.set(monthKey, new Map());
	}

	months.get(monthKey).set(day, cell);
}

Appending a node that is already in the document moves it. The tooltips keep working, the colors keep working, dark mode keeps working, and I wrote none of that. Then the old table gets hidden = true instead of being removed, so nothing that GitHub's own scripts hold a reference to breaks.

Each month's first cell needs an offset, which is plain date math:

const daysInMonth = new Date(year, month + 1, 0).getDate();
const firstWeekday = new Date(year, month, 1).getDay();

new Date(year, month + 1, 0) is the zeroth day of next month, which is the last day of this one. It handles leap years for free.

The part that actually took the time

Making the days stay square.

GitHub hardcodes style="width: 10px" on every cell. My layout is a responsive grid, so the width is fluid and the height was a fixed 14px, which meant the days turned into tall rectangles the moment the window got narrow.

The reflex is aspect-ratio: 1. That does not work: aspect-ratio is ignored on table cells. Their box sizing comes from the table algorithm, not from the cell.

The fix is to derive the height from the column width instead, using a container query unit. The month becomes a size container, and the cell height is computed from the month's own width:

.ghcv-month {
	container-type: inline-size;
}

.ghcv-month td {
	width: auto !important;
	/* the table is 100cqw wide, minus the 8 border-spacing gaps, over 7 columns */
	height: calc((100cqw - 15px) / 7) !important;
	padding: 0;
}

That gave me a second problem immediately. A size container contributes no inline size to its parent, and GitHub's calendar wrapper is a fit-content flex item, so it sizes itself from its contents. With nothing to measure, the wrapper collapsed to zero and took the whole calendar with it. One rule fixes it:

div:has(> .ghcv-container) {
	width: 100%;
}

The column widths also had to become percentages rather than pixels, because fixed widths cannot shrink and would overflow the month once its column got narrower than them. And table-layout: fixed only enforces equal columns when the table's own width is not auto, so the table needs width: 100% for the weekday letters to stop stretching their column.

Then the month titles stopped fitting, so:

@container (max-width: 80px) {
	.ghcv-month-name { display: none; }
	.ghcv-month-name-short { display: inline; }
}

January 2026 becomes Jan '26 on its own, with no JavaScript watching the resize.

Why the rejection was right

Refined GitHub's features fall into three shapes: hide noise, surface information GitHub already has but buries, or cut a click. Mine does none of those. It takes a component that works and re-presents it, because I prefer the other presentation. That is a redesign, and the project's name is a fair statement of what it is not.

There are two other reasons that have nothing to do with taste, and they are the ones I would think about before opening the next PR anywhere:

The maintenance lands on someone else. My code depends on js-calendar-graph-table, on td.ContributionCalendar-day[data-date], and on a :has() rule aimed at a wrapper element I do not control. GitHub changes that markup whenever it likes. Every break becomes an issue in a tracker I do not read.

There is no majority want. Refined GitHub ships features on by default. Roughly half of the people who saw my calendar would want it off, which means a setting, and a project with a hundred features has good reason to resist adding settings for opinions.

I could argue the calendar is genuinely better for finding a specific date. It is. It is also worse for the at-a-glance streak view, which is what most people open a profile for. That is a tradeoff, and declining tradeoffs is the entire job of a maintainer.

Porting it out

Refined GitHub has no plugin system, and it will not get one. Manifest V3 forbids executing remote code, so any "load a feature from a gist" mechanism is an instant store rejection. The extension's only user-facing extension point is a Custom CSS box, injected as a plain <style>:

if (options.customCss.trim().length > 0) {
	document.head.append(<style>{options.customCss}</style>);
}

CSS only. My feature rebuilds the DOM, so that door was closed too.

A standalone extension was the maintainer's suggestion, and I skipped it. Nobody finds an extension in a store search unless they already know the name. A userscript host does have search, and it has an index of scripts by site, which is discovery I do not have to earn.

The port was about an hour, because the feature barely touched the framework:

Refined GitHub Userscript
dom-chef JSX a 12-line element() helper
select-dom $$ querySelectorAll
pageDetect.isUserProfile @match plus a check that the table exists
observe() selector-observer a coalesced MutationObserver
import './x.css' a <style> injected on first hit

The MutationObserver matters more than it looks. GitHub swaps the calendar in through Turbo on navigation and again when you click a year, so running once on load gets you nothing most of the time.

No build step, no dependencies, @grant none. It also stands down if it ever sees Refined GitHub's own class on the table, in case they change their mind one day.

It is here: GitHub Contribution Calendar View.

Needs a browser with container query support, which is anything from the last couple of years. If GitHub renames a class, it will break, and now that is my problem rather than someone else's, which is the correct arrangement.