Skip to content

Instantly share code, notes, and snippets.

@aakashtyg
Last active July 22, 2026 07:50
Show Gist options
  • Select an option

  • Save aakashtyg/58f94b088296278d391fa5ba6dc8aa21 to your computer and use it in GitHub Desktop.

Select an option

Save aakashtyg/58f94b088296278d391fa5ba6dc8aa21 to your computer and use it in GitHub Desktop.
Add checkboxes as the first column in markdown tables rendered on Github
// ==UserScript==
// @name GitHub README table checkboxes
// @namespace local
// @match https://github.com/*
// @grant none
// ==/UserScript==
(() => {
// --- storage key: one bucket per repo + markdown file -----------------
function storageKey() {
const p = location.pathname;
const repo = (p.match(/^\/[^/]+\/[^/]+/) || ['/'])[0];
const file = (p.match(/\/(?:blob|tree)\/[^/]+\/(.*)$/) || [, ''])[1]
.replace(/README\.md$/i, '')
.replace(/\/$/, '');
return 'gh-table-checks:' + repo + (file ? '#' + file : '');
}
// --- main ------------------------------------------------------------
function run() {
const KEY = storageKey();
let state;
try {
state = JSON.parse(localStorage.getItem(KEY) || '{}');
} catch {
state = {};
}
const save = () => localStorage.setItem(KEY, JSON.stringify(state));
const tables = document.querySelectorAll('.markdown-body table, article table');
if (!tables.length) return;
let added = 0;
tables.forEach((table, ti) => {
if (table.dataset.checkboxed) return;
table.dataset.checkboxed = '1';
added++;
const headRow = table.querySelector('thead tr');
if (headRow) {
const th = document.createElement('th');
th.textContent = '\u2713';
th.style.width = '36px';
headRow.insertBefore(th, headRow.firstChild);
}
table.querySelectorAll('tbody tr').forEach((tr, ri) => {
// stable key from the row's question text, position as fallback
const label = (tr.cells[1]?.innerText || tr.innerText || '').trim().slice(0, 80);
const rowKey = `${ti}|${label || 'row' + ri}`;
const td = document.createElement('td');
td.style.textAlign = 'center';
const box = document.createElement('input');
box.type = 'checkbox';
box.style.cssText = 'width:16px;height:16px;cursor:pointer;';
box.checked = !!state[rowKey];
const paint = () => {
tr.style.opacity = box.checked ? '0.45' : '';
tr.style.textDecoration = box.checked ? 'line-through' : '';
};
box.addEventListener('change', () => {
if (box.checked) state[rowKey] = true;
else delete state[rowKey];
save();
paint();
});
paint();
td.appendChild(box);
tr.insertBefore(td, tr.firstChild);
});
});
if (added) {
const done = Object.keys(state).length;
console.log(`[table-checkboxes] ${added} table(s) processed, ${done} row(s) checked. Key: ${KEY}`);
}
// helpers, rebound to whatever page you're currently on
window.resetChecks = () => {
localStorage.removeItem(KEY);
location.reload();
};
window.exportChecks = () => console.log(JSON.stringify(state, null, 2));
window.importChecks = (json) => {
localStorage.setItem(KEY, typeof json === 'string' ? json : JSON.stringify(json));
location.reload();
};
}
// --- run now, and again after GitHub's client-side navigation ---------
run();
if (!window.__ghTableCheckboxHooked) {
window.__ghTableCheckboxHooked = true;
document.addEventListener('turbo:load', run);
document.addEventListener('turbo:render', run);
document.addEventListener('pjax:end', run);
// README bodies sometimes stream in after the page event fires
let pending;
new MutationObserver(() => {
clearTimeout(pending);
pending = setTimeout(run, 150);
}).observe(document.body, { childList: true, subtree: true });
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment