Add Releases tab after Code tab on GitHub repos
// ==UserScript==
// @name Add GitHub Releases Tab
// @namespace https://github.com/nvbangg/nvbangg-projects
// @version 1.7
// @description Add Releases tab after Code tab on GitHub repos
// @author nvbangg (https://github.com/nvbangg)
// @copyright Copyright (c) 2026 nvbangg (github.com/nvbangg)
// @match https://github.com/*/*
// @icon https://github.com/favicon.ico
// @license MIT
// @run-at document-start
// ==/UserScript==
(() => {
"use strict";
const TAB_ID = "releases-tab-link";
// Returns true when the current page is the releases section.
function isReleasesPage() {
return /\/releases(\/|$)/.test(location.pathname);
}
function getCodeAnchor() {
return document.querySelector(
'nav[aria-label="Repository"] a[data-tab-item="code"], nav[aria-label="Repository"] #code-tab'
);
}
// Sets or removes aria-current="page" on our tab to match GitHub's
// own active-tab convention (the only visual difference between tabs).
// When Releases is active, also strip aria-current from the Code tab so
// only one tab appears highlighted at a time.
function updateActiveState() {
const anchor = document.getElementById(TAB_ID);
const codeAnchor = getCodeAnchor();
if (!anchor) return;
if (isReleasesPage()) {
anchor.setAttribute("aria-current", "page");
// Code tab must not appear active while we're on the releases page.
if (codeAnchor) codeAnchor.removeAttribute("aria-current");
} else {
anchor.removeAttribute("aria-current");
// Let GitHub's own rendering decide whether Code is active again;
// we only restore it if it was previously cleared by us and GitHub
// hasn't already re-set it via Turbo.
if (codeAnchor && !codeAnchor.hasAttribute("aria-current")) {
const onCodePage = /^\/[^/]+\/[^/]+(\/tree\/|\/blob\/|\/)?$/.test(location.pathname);
if (onCodePage) codeAnchor.setAttribute("aria-current", "page");
}
}
}
function insertReleasesTab() {
const nav = document.querySelector('nav[aria-label="Repository"]');
if (!nav || nav.querySelector(`#${TAB_ID}`)) {
// Tab already exists — just sync the active state.
updateActiveState();
return;
}
// Bail if releases tab already exists natively.
if (nav.querySelector('a[href$="/releases"]')) return;
const codeAnchor = nav.querySelector('a[data-tab-item="code"], #code-tab');
if (!codeAnchor) return;
const urlParts = location.pathname.split("/");
if (urlParts.length < 3) return;
const releasesHref = `/${urlParts[1]}/${urlParts[2]}/releases`;
// Read class names live from the Code tab so we match whatever
// GitHub is currently using — no hardcoded strings that can go stale.
const liClass = codeAnchor.closest("li")?.className ?? "";
const aClass = codeAnchor.className ?? "";
// Build the SVG using the same attributes as every other tab icon.
const existingSvg = codeAnchor.querySelector("svg");
const svgAttrs = existingSvg
? `data-component="Octicon" aria-hidden="true" focusable="false" class="octicon octicon-tag" viewBox="0 0 16 16" width="${existingSvg.getAttribute("width") || 16}" height="${existingSvg.getAttribute("height") || 16}" fill="currentColor" display="inline-block" overflow="visible" style="vertical-align: text-bottom;"`
: `aria-hidden="true" focusable="false" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"`;
// aria-current="page" is set here immediately if we are already on the
// releases page (e.g. hard reload directly onto a releases URL).
const ariaCurrent = isReleasesPage() ? 'aria-current="page"' : "";
const html = `
<li class="${liClass}">
<a id="${TAB_ID}"
href="${releasesHref}"
class="${aClass}"
${ariaCurrent}
data-tab-item="releases"
data-turbo-frame="repo-content-turbo-frame"
data-discover="true"
aria-label="Releases">
<span data-component="icon">
<svg ${svgAttrs}>
<path d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"></path>
</svg>
</span>
<span data-component="text" data-content="Releases">Releases</span>
</a>
</li>`;
const codeLi = codeAnchor.closest("li") ?? codeAnchor;
codeLi.insertAdjacentHTML("afterend", html);
// Guard against GitHub's overflow system hiding our tab.
const injectedLi = document.getElementById(TAB_ID)?.closest("li");
const injectedAnchor = document.getElementById(TAB_ID);
if (!injectedLi || !injectedAnchor) return;
new MutationObserver(() => {
if (injectedLi.getAttribute("aria-hidden") === "true")
injectedLi.removeAttribute("aria-hidden");
if (injectedAnchor.getAttribute("tabindex") === "-1")
injectedAnchor.removeAttribute("tabindex");
}).observe(injectedLi, { attributes: true });
}
// ── SPA navigation ────────────────────────────────────────────────────────
// insertReleasesTab re-inserts the tab after Turbo swaps the nav out,
// and also calls updateActiveState() when the tab already exists.
// popstate covers back/forward without a Turbo event.
document.addEventListener("turbo:load", insertReleasesTab);
document.addEventListener("turbolinks:load", insertReleasesTab);
window.addEventListener("popstate", insertReleasesTab);
insertReleasesTab();
new MutationObserver(insertReleasesTab).observe(document.documentElement, {
childList: true,
subtree: true,
});
})();