NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Gmail Quick Links
// @namespace https://openuserjs.org/users/nascent
// @version 1.2
// @description Inserts a Gmail Quick Links container above the folders area, sorted alphabetically. The "Add Quick Link" button uses the current browser URL and prompts only for a label. Includes options to export/import quick links using a JSON file.
// @author nascent
// @match https://mail.google.com/*
// @updateURL https://openuserjs.org/meta/nascent/Gmail_Quick_Links.meta.js
// @downloadURL https://openuserjs.org/install/nascent/Gmail_Quick_Links.user.js
// @icon https://www.google.com/s2/favicons?sz=64&domain=google.com
// @copyright 2025, nascent (https://openuserjs.org/users/nascent)
// @license MIT
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant unsafeWindow
// @noframes
// ==/UserScript==
(function () {
"use strict";
// Prevent double execution by checking the flag on the real window (unsafeWindow)
// instead of the sandbox window.
const scope = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
if (scope._gmailQuickLinksLoaded) return;
scope._gmailQuickLinksLoaded = true;
/************************************************************************
* Inject Custom CSS
*
* This CSS block scales down the quick links container (relative sizing)
* and supplies fallback icons for empty glyph spans.
************************************************************************/
if (!document.getElementById("custom-gmail-quicklinks-style")) {
const styleEl = document.createElement("style");
styleEl.id = "custom-gmail-quicklinks-style";
styleEl.textContent = `
/* Scale down the quick links container */
#gmailQuickLinksContainer {
font-size: 0.8em !important;
}
/* Fallback icons if Gmail’s native glyphs aren’t applied */
#gmailQuickLinksContainer .glyph.global::before {
content: "🌐";
}
#gmailQuickLinksContainer .glyph.rename::before {
content: "✎";
font-weight: bold;
}
#gmailQuickLinksContainer .glyph.delete::before {
content: "🗑";
}
/* Clear the float */
#gmailQuickLinksContainer .clear {
clear: both;
}
/* Match link color to Gmail's sidebar text */
#gmailQuickLinksContainer a,
#gmailQuickLinksContainer #addQuickLinkButton,
#gmailQuickLinksContainer .glyph.rename,
#gmailQuickLinksContainer .glyph.delete {
color: var(--gm-link-color, inherit) !important;
}
`;
document.head.appendChild(styleEl);
}
/************************************************************************
* Trusted Types Setup
*
* Gmail enforces Trusted Types so wrap HTML strings via a policy.
************************************************************************/
const trustedPolicy = window.trustedTypes
? window.trustedTypes.createPolicy("myPolicy", { createHTML: input => input })
: null;
/************************************************************************
* Default Quick Links
*
* Only the two Inbox links are provided as defaults.
************************************************************************/
const defaultQuickLinks = [
{
label: "Inbox",
href: "#advanced-search/is_unread=true&query=is:inbox+-category:promotions+-is:starred",
title: "Inbox"
},
{
label: "Inbox (No Categories)",
href: "#advanced-search/is_unread=true&query=is:inbox+-category:promotions+-category:social+-category:updates+-category:forums+-is:starred",
title: "Inbox (No Categories)"
}
];
/************************************************************************
* Utility Functions: Load, Save, and Sort Quick Links
************************************************************************/
function loadQuickLinks() {
const stored = GM_getValue("quickLinks", null);
if (stored) {
try {
return JSON.parse(stored);
} catch (e) {
console.error("Failed to parse quickLinks from storage", e);
}
}
return defaultQuickLinks;
}
function sortQuickLinks(links) {
return links.sort((a, b) =>
a.label.toLowerCase().localeCompare(b.label.toLowerCase())
);
}
function saveQuickLinks(links) {
GM_setValue("quickLinks", JSON.stringify(links));
}
/************************************************************************
* Render Quick Links List
*
* Builds the display for each quick link.
************************************************************************/
function renderQuickLinks() {
const listContainer = document.getElementById("listContainer");
if (!listContainer) return;
const initialHTML = '<div style="padding-left: 30px;"></div>';
listContainer.innerHTML = trustedPolicy
? trustedPolicy.createHTML(initialHTML)
: initialHTML;
const innerContainer = listContainer.firstElementChild;
const links = loadQuickLinks();
links.forEach((link, index) => {
const linkDiv = document.createElement("div");
// Create the anchor element.
const a = document.createElement("a");
a.className = "n0";
a.href = link.href;
a.title = link.title || link.label;
a.style.textDecoration = "underline";
a.textContent = link.label;
linkDiv.appendChild(a);
// Rename icon
const renameSpan = document.createElement("span");
renameSpan.className = "glyph rename";
renameSpan.title = "rename";
renameSpan.style.cursor = "pointer";
renameSpan.style.marginLeft = "5px";
renameSpan.addEventListener("click", (e) => {
e.stopPropagation();
e.preventDefault();
const newLabel = prompt("Enter new label", link.label);
if (newLabel) {
let current = loadQuickLinks();
current[index].label = newLabel;
current[index].title = newLabel;
// Sort links alphabetically before saving.
current = sortQuickLinks(current);
saveQuickLinks(current);
renderQuickLinks();
}
});
linkDiv.appendChild(renameSpan);
// Delete icon
const deleteSpan = document.createElement("span");
deleteSpan.className = "glyph delete";
deleteSpan.title = "delete";
deleteSpan.style.cursor = "pointer";
deleteSpan.style.marginLeft = "5px";
deleteSpan.addEventListener("click", (e) => {
e.stopPropagation();
e.preventDefault();
if (confirm("Delete this quick link?")) {
let current = loadQuickLinks();
current.splice(index, 1);
// Sort links alphabetically before saving.
current = sortQuickLinks(current);
saveQuickLinks(current);
renderQuickLinks();
}
});
linkDiv.appendChild(deleteSpan);
// Clear div for layout.
const clearDiv = document.createElement("div");
clearDiv.className = "clear";
linkDiv.appendChild(clearDiv);
innerContainer.appendChild(linkDiv);
});
}
/************************************************************************
* Insert Quick Links Container
*
* Inserts the Quick Links container (header and list) into Gmail’s
* navigation area as the first child so it appears above the folders/labels.
************************************************************************/
function applySidebarTextColor() {
const container = document.getElementById("gmailQuickLinksContainer");
if (!container) return;
// Sample from the "Quick Links" heading — it's already visible and themed by Gmail
const heading = container.querySelector("h2.pw");
if (heading) {
const color = getComputedStyle(heading).color;
if (color) {
container.style.setProperty("--gm-link-color", color);
return;
}
}
// Fallback: sample from any Gmail sidebar label
const sidebarLabel = document.querySelector('[role="navigation"] .TN .nU > .n0')
|| document.querySelector('[role="navigation"] a');
if (sidebarLabel) {
const color = getComputedStyle(sidebarLabel).color;
if (color) container.style.setProperty("--gm-link-color", color);
}
}
function insertQuickLinksContainer() {
const nav = document.querySelector('[role="navigation"]');
if (nav && !document.getElementById("gmailQuickLinksContainer")) {
const html = `
<div id="gmailQuickLinksContainer">
<div data-reactroot="" id="gmailQuickLinks" class="">
<div>
<div class="r" style="display: flex; align-items: baseline; justify-content: space-between;">
<div style="cursor: auto; position: relative; overflow: hidden; vertical-align: middle; outline: none; font-size: 100%;">
<span class="glyph info" title="info/help"></span>
<h2 class="pw">Quick Links</h2>
</div>
<div class="n0" id="addQuickLinkButton" title="Add Quick Link" style="text-decoration: underline; cursor:pointer;">
Add Quick Link
</div>
</div>
<div id="listContainer" style="padding-bottom: 10px;">
<div style="padding-left: 30px;"></div>
</div>
</div>
</div>
</div>
`;
const safeHTML = trustedPolicy ? trustedPolicy.createHTML(html) : html;
const fragment = document.createRange().createContextualFragment(safeHTML);
nav.insertBefore(fragment, nav.firstChild);
// Set up the "Add Quick Link" button.
const addButton = document.getElementById("addQuickLinkButton");
if (addButton) {
addButton.addEventListener("click", () => {
const label = prompt("Enter Quick Link Label:");
if (!label) return;
// Automatically use the current browser location for the URL.
const href = window.location.href;
let current = loadQuickLinks();
current.push({ label: label, href: href, title: label });
// Sort links alphabetically before saving.
current = sortQuickLinks(current);
saveQuickLinks(current);
renderQuickLinks();
});
}
renderQuickLinks();
applySidebarTextColor();
console.log("Custom Gmail Quick Links container inserted.");
}
}
/************************************************************************
* Export and Import Functions
*
* Export: Converts the quick links to JSON and triggers a download as a file.
* Import: Opens a file chooser and, upon file selection, reads and parses the
* JSON to overwrite existing quick links.
************************************************************************/
function exportQuickLinks() {
const links = loadQuickLinks();
const json = JSON.stringify(links, null, 2);
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
if (typeof GM_download === "function") {
// Use GM_download if available.
GM_download({ url: url, name: "quicklinks.json" });
} else {
const a = document.createElement("a");
a.href = url;
a.download = "quicklinks.json";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
function importQuickLinks() {
const input = document.createElement("input");
input.type = "file";
input.accept = "application/json";
input.style.display = "none";
input.addEventListener("change", function (event) {
const file = event.target.files[0];
if (!file) {
input.remove();
return;
}
const reader = new FileReader();
reader.onload = function (e) {
try {
const data = JSON.parse(e.target.result);
saveQuickLinks(data);
renderQuickLinks();
alert("Quick links imported successfully.");
} catch (err) {
alert("Error importing quick links: " + err);
}
input.remove();
};
reader.readAsText(file);
});
document.body.appendChild(input);
input.click();
}
/************************************************************************
* Observe Gmail's Dynamic DOM
*
* Gmail’s UI is dynamic, so a MutationObserver reinserts container when needed.
************************************************************************/
const observer = new MutationObserver(() => {
insertQuickLinksContainer();
applySidebarTextColor();
});
observer.observe(document.body, { childList: true, subtree: true });
// Insert the container immediately.
insertQuickLinksContainer();
/************************************************************************
* Tampermonkey Menu Commands
*
* Registers three menu items:
* Export Quicklinks – triggers a file download of your quick links as JSON.
* Import Quicklinks – allows you to select a JSON file to overwrite your quick links.
* Reset Quicklinks – reset links to default.
************************************************************************/
if (typeof GM_registerMenuCommand !== "undefined") {
GM_registerMenuCommand("Export Quicklinks", exportQuickLinks);
GM_registerMenuCommand("Import Quicklinks", importQuickLinks);
GM_registerMenuCommand("Reset Quicklinks", function () {
if (confirm("Are you sure you want to reset quick links to defaults? This will erase all your custom links.")) {
saveQuickLinks(defaultQuickLinks);
renderQuickLinks();
alert("Quick links have been reset to defaults.");
}
});
}
})();