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 http://tampermonkey.net/
// @version 1.0
// @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
// ==/UserScript==
(function () {
"use strict";
/************************************************************************
* 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;
}
`;
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);
// Global icon
// I never used this so not implementing - global by default
/*const globalSpan = document.createElement("span");
globalSpan.className = "glyph global";
globalSpan.title = "toggle global/single";
globalSpan.style.cursor = "pointer";
globalSpan.style.marginLeft = "5px";
linkDiv.appendChild(globalSpan);*/
// 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 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();
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) return;
const reader = new FileReader();
reader.onload = function (e) {
try {
const data = JSON.parse(e.target.result);
if (confirm("This will overwrite your existing quick links. Continue?")) {
saveQuickLinks(data);
renderQuickLinks();
alert("Quick links imported successfully.");
}
}
catch (err) {
alert("Error importing quick links: " + err);
}
};
reader.readAsText(file);
});
document.body.appendChild(input);
input.click();
input.remove();
}
/************************************************************************
* Observe Gmail's Dynamic DOM
*
* Gmail’s UI is dynamic, so a MutationObserver reinserts container when needed.
************************************************************************/
const observer = new MutationObserver(() => {
insertQuickLinksContainer();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Insert the container immediately.
insertQuickLinksContainer();
/************************************************************************
* Tampermonkey Menu Commands
*
* Registers two 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.
************************************************************************/
if (typeof GM_registerMenuCommand !== "undefined") {
GM_registerMenuCommand("Export Quicklinks", exportQuickLinks);
GM_registerMenuCommand("Import Quicklinks", importQuickLinks);
}
})();