Skip to content

Instantly share code, notes, and snippets.

@rafaelsteil
Last active September 9, 2026 13:59
Show Gist options
  • Select an option

  • Save rafaelsteil/3f6a569c68b2e4156796e7b3659bd0bc to your computer and use it in GitHub Desktop.

Select an option

Save rafaelsteil/3f6a569c68b2e4156796e7b3659bd0bc to your computer and use it in GitHub Desktop.
TamperMonkey script to open GitHub README and markdown images in a lightbox instead of the blob page
// ==UserScript==
// @name GitHub Image Lightbox
// @namespace https://gist.github.com/rafaelsteil/3f6a569c68b2e4156796e7b3659bd0bc
// @version 1.0.0
// @description Open GitHub README and markdown images in a lightbox instead of the blob page
// @author Rafael Steil
// @match https://github.com/*
// @match https://gist.github.com/*
// @run-at document-end
// @noframes
// @grant none
// ==/UserScript==
(function () {
"use strict";
const ROOT_ID = "gilb-root";
if (document.getElementById(ROOT_ID)) return;
const IMAGE_EXT = /\.(?:png|jpe?g|gif|webp|svg|bmp|ico|avif|apng|tif|tiff)(?:$|[?#])/i;
const GITHUB_ASSET = /(?:user-images\.githubusercontent\.com|private-user-images\.githubusercontent\.com|media\.githubusercontent\.com|\/user-attachments\/assets\/|github\.com\/[^/]+\/[^/]+\/assets\/)/i;
const MARKDOWN_IMAGE = ".markdown-body img";
let activeIndex = -1;
let gallery = [];
let lastFocus = null;
const style = document.createElement("style");
style.textContent = `
#${ROOT_ID} {
position: fixed;
inset: 0;
z-index: 2147483646;
display: flex;
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
color: #f0f6fc;
}
#${ROOT_ID}[hidden] { display: none !important; }
#${ROOT_ID} .gilb-backdrop {
position: absolute;
inset: 0;
background: rgba(1, 4, 9, 0.82);
backdrop-filter: blur(6px);
}
#${ROOT_ID} .gilb-chrome {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px 20px;
pointer-events: none;
}
#${ROOT_ID} .gilb-actions {
display: flex;
gap: 10px;
pointer-events: auto;
}
#${ROOT_ID} .gilb-caption {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
color: #9198a1;
pointer-events: none;
}
#${ROOT_ID} a.gilb-btn,
#${ROOT_ID} button.gilb-btn {
appearance: none;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: 1px solid #3d444d;
border-radius: 6px;
background: #212830;
color: #f0f6fc;
font: inherit;
font-size: 13px;
font-weight: 600;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}
#${ROOT_ID} a.gilb-btn:hover,
#${ROOT_ID} button.gilb-btn:hover {
background: #262c36;
border-color: #656c76;
}
#${ROOT_ID} a.gilb-btn:focus-visible,
#${ROOT_ID} button.gilb-btn:focus-visible {
outline: 2px solid #1f6feb;
outline-offset: 2px;
}
#${ROOT_ID} .gilb-stage {
position: relative;
z-index: 1;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-height: 0;
padding: 0 72px 24px;
}
#${ROOT_ID} .gilb-image {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
background: #0d1117;
}
#${ROOT_ID} .gilb-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 40px;
height: 40px;
border: 1px solid #3d444d;
border-radius: 50%;
background: #212830;
color: #f0f6fc;
font-size: 22px;
line-height: 1;
cursor: pointer;
}
#${ROOT_ID} .gilb-nav:hover { background: #262c36; }
#${ROOT_ID} .gilb-nav[hidden] { display: none; }
#${ROOT_ID} .gilb-prev { left: 16px; }
#${ROOT_ID} .gilb-next { right: 16px; }
#${ROOT_ID} .gilb-error {
color: #ffa198;
font-size: 14px;
}
body.gilb-lock { overflow: hidden !important; }
.markdown-body img.gilb-zoom { cursor: zoom-in; }
`;
document.documentElement.appendChild(style);
const root = document.createElement("div");
root.id = ROOT_ID;
root.hidden = true;
root.setAttribute("role", "dialog");
root.setAttribute("aria-modal", "true");
root.setAttribute("aria-label", "Image preview");
root.innerHTML = `
<div class="gilb-backdrop" data-gilb-close></div>
<div class="gilb-chrome">
<div class="gilb-actions">
<button type="button" class="gilb-btn gilb-close" data-gilb-close>Close</button>
</div>
<div class="gilb-caption" aria-live="polite"></div>
<div class="gilb-actions">
<a class="gilb-btn gilb-open" href="#" target="_blank" rel="noopener noreferrer">Open original</a>
</div>
</div>
<div class="gilb-stage">
<button type="button" class="gilb-nav gilb-prev" aria-label="Previous image">‹</button>
<img class="gilb-image" alt="">
<div class="gilb-error" hidden>Could not load this image.</div>
<button type="button" class="gilb-nav gilb-next" aria-label="Next image">›</button>
</div>
`;
document.documentElement.appendChild(root);
const els = {
image: root.querySelector(".gilb-image"),
error: root.querySelector(".gilb-error"),
caption: root.querySelector(".gilb-caption"),
open: root.querySelector(".gilb-open"),
prev: root.querySelector(".gilb-prev"),
next: root.querySelector(".gilb-next"),
close: root.querySelector(".gilb-close"),
};
function isModifiedClick(event) {
return event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
}
function isImageUrl(url) {
if (!url) return false;
try {
const parsed = new URL(url, location.origin);
return IMAGE_EXT.test(parsed.pathname) || GITHUB_ASSET.test(parsed.href);
} catch {
return IMAGE_EXT.test(url) || GITHUB_ASSET.test(url);
}
}
function shouldLightbox(img, link) {
if (!img || img.closest("#" + ROOT_ID)) return false;
if (!img.closest(".markdown-body")) return false;
if (!link) return isImageUrl(img.currentSrc || img.src);
// Only intercept navigations that would open an image (blob/raw/asset/file),
// not badges or images that wrap a regular page link.
return isImageUrl(link.href);
}
function collectGallery() {
return Array.from(document.querySelectorAll(MARKDOWN_IMAGE)).filter((img) => {
const link = img.closest("a");
return shouldLightbox(img, link) && (img.currentSrc || img.src);
});
}
function originalUrlFor(img) {
const link = img.closest("a");
if (link && link.href) return link.href;
return img.currentSrc || img.src;
}
function show(index) {
const img = gallery[index];
if (!img) return;
activeIndex = index;
const src = img.currentSrc || img.src;
const original = originalUrlFor(img);
els.image.hidden = false;
els.error.hidden = true;
els.image.src = src;
els.image.alt = img.alt || "";
els.open.href = original;
els.caption.textContent = img.alt || original.replace(/^https?:\/\/github\.com/, "");
els.prev.hidden = gallery.length < 2;
els.next.hidden = gallery.length < 2;
root.hidden = false;
document.body.classList.add("gilb-lock");
els.close.focus();
}
function openFrom(img) {
gallery = collectGallery();
const index = gallery.indexOf(img);
lastFocus = document.activeElement;
show(index === -1 ? 0 : index);
}
function close() {
if (root.hidden) return;
root.hidden = true;
els.image.removeAttribute("src");
document.body.classList.remove("gilb-lock");
activeIndex = -1;
if (lastFocus && typeof lastFocus.focus === "function") lastFocus.focus();
}
function step(delta) {
if (gallery.length < 2) return;
const next = (activeIndex + delta + gallery.length) % gallery.length;
show(next);
}
els.image.addEventListener("error", () => {
els.image.hidden = true;
els.error.hidden = false;
});
root.addEventListener("click", (event) => {
if (event.target.closest("[data-gilb-close]")) {
event.preventDefault();
close();
}
});
els.prev.addEventListener("click", (event) => {
event.preventDefault();
step(-1);
});
els.next.addEventListener("click", (event) => {
event.preventDefault();
step(1);
});
document.addEventListener(
"click",
(event) => {
if (isModifiedClick(event) || event.defaultPrevented) return;
const img = event.target.closest(MARKDOWN_IMAGE);
if (!img) return;
const link = img.closest("a");
if (!shouldLightbox(img, link)) return;
event.preventDefault();
event.stopImmediatePropagation();
openFrom(img);
},
true
);
document.addEventListener("keydown", (event) => {
if (root.hidden) return;
if (event.key === "Escape") {
event.preventDefault();
close();
} else if (event.key === "ArrowLeft") {
event.preventDefault();
step(-1);
} else if (event.key === "ArrowRight") {
event.preventDefault();
step(1);
}
});
function markZoomable() {
document.querySelectorAll(MARKDOWN_IMAGE).forEach((img) => {
img.classList.toggle("gilb-zoom", shouldLightbox(img, img.closest("a")));
});
}
markZoomable();
document.addEventListener("turbo:load", markZoomable);
document.addEventListener("turbo:render", markZoomable);
let markTimer = 0;
new MutationObserver(() => {
if (markTimer) return;
markTimer = requestAnimationFrame(() => {
markTimer = 0;
markZoomable();
});
}).observe(document.body || document.documentElement, {
childList: true,
subtree: true,
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment