// ==UserScript==
// @name Declutter Pinterest
// @namespace August4067
// @version 0.9.0
// @description Removes intrusive Pinterest shopping promotions, ads, and clutter, and makes the website more user-friendly
// @license MIT
// @match https://www.pinterest.com/*
// @match https://*.pinterest.com/*
// @match https://*.pinterest.co.uk/*
// @match https://*.pinterest.fr/*
// @match https://*.pinterest.de/*
// @match https://*.pinterest.ca/*
// @match https://*.pinterest.jp/*
// @match https://*.pinterest.it/*
// @match https://*.pinterest.au/*
// @icon https://www.pinterest.com/favicon.ico
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @sandbox Javascript
// ==/UserScript==
/*--- waitForKeyElements(): A utility function, for Greasemonkey scripts,
that detects and handles AJAXed content.
Usage example:
waitForKeyElements (
"div.comments"
, commentCallbackFunction
);
//--- Page-specific function to do what we want when the node is found.
function commentCallbackFunction (jNode) {
jNode.text ("This comment changed by waitForKeyElements().");
}
IMPORTANT: This function requires your script to have loaded jQuery.
*/
// Pulled from: https://gist.github.com/raw/2625891/waitForKeyElements.js
function waitForKeyElements(
selectorTxt /* Required: The jQuery selector string that
specifies the desired element(s).
*/,
actionFunction /* Required: The code to run when elements are
found. It is passed a jNode to the matched
element.
*/,
bWaitOnce /* Optional: If false, will continue to scan for
new elements even after the first match is
found.
*/,
iframeSelector /* Optional: If set, identifies the iframe to
search.
*/,
) {
var targetNodes, btargetsFound;
if (typeof iframeSelector == "undefined") targetNodes = $(selectorTxt);
else targetNodes = $(iframeSelector).contents().find(selectorTxt);
if (targetNodes && targetNodes.length > 0) {
btargetsFound = true;
/*--- Found target node(s). Go through each and act if they
are new.
*/
targetNodes.each(function () {
var jThis = $(this);
var alreadyFound = jThis.data("alreadyFound") || false;
if (!alreadyFound) {
//--- Call the payload function.
var cancelFound = actionFunction(jThis);
if (cancelFound) btargetsFound = false;
else jThis.data("alreadyFound", true);
}
});
} else {
btargetsFound = false;
}
//--- Get the timer-control variable for this selector.
var controlObj = waitForKeyElements.controlObj || {};
var controlKey = selectorTxt.replace(/[^\w]/g, "_");
var timeControl = controlObj[controlKey];
//--- Now set or clear the timer as appropriate.
if (btargetsFound && bWaitOnce && timeControl) {
//--- The only condition where we need to clear the timer.
clearInterval(timeControl);
delete controlObj[controlKey];
} else {
//--- Set a timer, if needed.
if (!timeControl) {
timeControl = setInterval(function () {
waitForKeyElements(
selectorTxt,
actionFunction,
bWaitOnce,
iframeSelector,
);
}, 300);
controlObj[controlKey] = timeControl;
}
}
waitForKeyElements.controlObj = controlObj;
}
// We will set the Pinterest page title to this, to remove
// the flashing title notifications like Pinterest (2)
const ORIGINAL_TITLE = 'Pinterest';
const SETTINGS_CONFIG = {
removeShoppablePins: {
displayName: "Remove shoppable pins",
default: true
},
removePinFooters: {
displayName: "Remove pin footers",
default: false
},
removeComments: {
displayName: "Remove pin comments",
default: false
},
removeNavbarMessagesIcon: {
displayName: "Remove navbar messages icon",
default: false
},
removeNavbarNotificationIcon: {
displayName: "Remove navbar notifications icon",
default: false
}
};
class Setting {
constructor(name, config) {
this.name = name;
this.displayName = config.displayName;
this.default = config.default;
}
currentValue() {
return GM_getValue(this.name, this.default);
}
toggleSetting() {
GM_setValue(this.name, !this.currentValue());
}
}
// Create settings object by mapping config to Setting instances
const SETTINGS = Object.fromEntries(
Object.entries(SETTINGS_CONFIG).map(([name, config]) => [
name,
new Setting(name, config)
])
);
// MENU SETTINGS
function toggleMenuSetting(settingName) {
var setting = SETTINGS[settingName];
setting.toggleSetting();
updateSettingsMenu();
console.debug(`Setting ${settingName} set to: ${setting.currentValue()}}`);
location.reload();
}
function updateSettingsMenu() {
for (const [setting_name, setting] of Object.entries(SETTINGS)) {
GM_registerMenuCommand(
`${setting.displayName}: ${setting.currentValue() ? "Enabled" : "Disabled"}`,
() => {
toggleMenuSetting(setting_name);
},
);
}
}
// HELPER FUNCTIONS
function waitAndRemove(selector, removeFunction) {
if (removeFunction == undefined) {
removeFunction = (elem) => elem.remove();
}
waitForKeyElements(selector, function (node) {
if (node && node.length > 0) {
removeFunction(node[0]);
}
});
}
/**
* Hide an element by setting its display style to "none" if it exists
* @param {HTMLElement} element - The DOM element to hide
*/
function hideElement(element) {
if (element) {
element.style.display = "none";
}
}
/**
* Return an array of pins, filtering out those that already have
* style.display == "none" (pins that we have removed)
* @param {*} pins
* @returns array of pins
*/
function filterRemovedPins(pins) {
return pins ? pins.filter((pin) => pin.style.display !== "none") : [];
}
// DECLUTTER BOARDS
/**
* Clean the Shop button from the top of boards (from the 3 button group with "Shop", "Organize", and "More ideas")
*/
function cleanShopButtonsFromBoard() {
console.debug("Cleaning Shop buttons from board");
waitForKeyElements('div[data-test-id="board-tools"]', function (node) {
var shopButton = node[0].querySelector('div[data-test-id="Shop"]');
hideElement(shopButton);
console.debug("Removed Shop button from top of board");
});
}
/**
* Remove the banner of shopping pins wherever we find it (with title "Shop products inspired by this board").
* They sometimes show up at the top of boards, at the bottom of boards, and at the top of searches.
*/
function cleanShopByBanners() {
waitForKeyElements('div[data-test-id="sf-header-heading"]', function (node) {
var shopByBannerAtTopOfBoard = node[0].closest(
'div[class="PKX zI7 iyn Hsu"]',
);
hideElement(shopByBannerAtTopOfBoard);
console.debug("Removed shop by banner from top of board");
if (node[0].closest('div[data-test-id="base-board-pin-grid"]')) {
var shopByBannerAtBottomOfBoard = node[0].closest(
'div[class="gcw zI7 iyn Hsu"]',
);
hideElement(shopByBannerAtBottomOfBoard);
console.debug("Removed shop by banner from bottom of board");
}
var shopByBannerAtTopOfSearch = node[0].closest('div[role="listitem"]');
hideElement(shopByBannerAtTopOfSearch);
console.debug("Removed shop by banner from top of search results");
});
}
// DECLUTTER NAVBAR
/**
* The search bar now has dynamic placeholder text with suggested searches. These are distracting, and we will remove them.
*/
function cleanSearchBarDynamicText(searchBox) {
searchBox
.querySelector('div[data-test-id="dynamic-search-placeholder"]')
?.remove();
}
function cleanSearchBarSuggestions(searchBox) {
var suggestionsMenu = searchBox.querySelector('div[id="SuggestionsMenu"]');
if (!suggestionsMenu) {
return;
}
var popularOnPinterestSuggestions = suggestionsMenu.querySelector(
'div[title="Popular on Pinterest"]',
);
if (popularOnPinterestSuggestions) {
var popularOnPinterestBanner = popularOnPinterestSuggestions.closest(
'div[class="jzS un8 L4V jDD"]',
);
hideElement(popularOnPinterestBanner);
console.debug("Removed Popular on Pinterest search suggestions");
}
}
/**
* A series of tabs is at the top of search results to refine results by your interests ("all", "holiday finds", "my board 1", "my board 2", etc).
* We will remove the shopping / promoted tabs
*/
function cleanNavTabCarousel() {
waitForKeyElements('div[class="localNavTabCarousel"]', function (nodes) {
var navTabCarousel = nodes[0];
if (!navTabCarousel) {
return;
}
const promotedTabs = new Set(["holiday finds"]);
navTabCarousel.querySelectorAll('div[class="xuA"]').forEach((item) => {
if (!item.innerText) {
return;
}
var navTabText = item.innerText.trim().toLowerCase();
if (promotedTabs.has(navTabText)) {
hideElement(item);
console.debug(`Hid promoted nav tab: ${navTabText}`);
}
});
});
}
function removeBellIcon() {
if (SETTINGS.removeNavbarNotificationIcon.currentValue()) {
const bellIconDiv = document.querySelector('div[data-test-id="bell-icon"]');
hideElement(bellIconDiv);
console.debug("Removed bell icon from navbar.");
}
}
function removeMessagesIcon() {
if (SETTINGS.removeNavbarMessagesIcon.currentValue()) {
const messagesIconDiv = document.querySelector(
'div[aria-label="Messages"]',
);
if (messagesIconDiv) {
var messagesParent = messagesIconDiv.closest(
'div[class="XiG zI7 iyn Hsu"]',
);
hideElement(messagesParent);
console.debug("Removed messages button");
}
}
}
function removeExploreTabNotificationsIcon() {
console.debug("Removing notifications icon from Explore tab (top nav and sidebar)");
// --- Remove notification icon from Explore tab in the top nav (old behavior)
var exploreTab = document.querySelector('div[data-test-id="today-tab"]');
if (exploreTab) {
var notificationsIcon = exploreTab.querySelector(
'div[aria-label="Notifications"]',
);
hideElement(notificationsIcon);
if (notificationsIcon) {
console.debug("Removed notifications icon from Explore tab (top nav)");
}
}
// --- Remove notification badge from Explore tab in the sidebar (new behavior)
// Find the Explore tab link in the sidebar
var exploreTabLink = document.querySelector('a[data-test-id="today-tab"]');
if (exploreTabLink) {
// The parent of the link is the icon container, its parent is the sidebar item
var iconContainer = exploreTabLink.closest('div[class*="XiG"]');
var sidebarItem = iconContainer?.parentElement?.parentElement;
if (sidebarItem) {
// The notification badge is a sibling div with class "MIw" and pointer-events: none
var notificationBadge = sidebarItem.parentElement?.querySelector('.MIw[style*="pointer-events: none"]');
if (notificationBadge) {
hideElement(notificationBadge);
console.debug("Removed notifications badge from Explore tab (sidebar)");
}
}
}
}
function cleanNavBarCallback(mutationsList) {
console.debug("Cleaning navbar");
removeBellIcon();
removeMessagesIcon();
var callbackCount = 0;
waitForKeyElements('div[data-test-id="today-tab"]', function (nodes) {
new MutationObserver((mutations, observer) => {
removeExploreTabNotificationsIcon();
}).observe(nodes[0], {
childList: true,
subtree: true,
});
});
removeExploreTabNotificationsIcon();
waitForKeyElements('div[id="searchBoxContainer"]', function (nodes) {
new MutationObserver((mutations, observer) => {
cleanSearchBarSuggestions(nodes[0]);
cleanSearchBarDynamicText(nodes[0]);
}).observe(nodes[0], {
childList: true,
subtree: true,
});
});
}
function cleanVerticalNavBar(verticalNavContent) {
// TODO: Remove notification badges from explore icon
waitForKeyElements('div[aria-label="Messages"]', function (nodes) {
if (SETTINGS.removeNavbarMessagesIcon.currentValue()) {
if (nodes && nodes.length > 0) {
var closest = nodes[0].closest('div[class="xuA"]');
hideElement(closest);
}
}
});
waitForKeyElements('div[aria-label="Notifications"]', function (nodes) {
if (SETTINGS.removeNavbarNotificationIcon.currentValue()) {
if (nodes && nodes.length > 0) {
var closest = nodes[0].closest('div[class="xuA"]');
hideElement(closest);
}
}
});
}
function cleanNavBar() {
// This is the nav bar at the top of the page
waitForKeyElements('div[id="HeaderContent"]', function (nodes) {
const headerContent = document.getElementById("HeaderContent");
if (headerContent) {
var observerCallCount = 0;
const observer = new MutationObserver((mutations, observer) => {
cleanNavBarCallback(mutations);
if (++observerCallCount >= 5) {
observer.disconnect();
console.debug("Disconnected cleanNavBar() mutation observer");
}
});
observer.observe(headerContent, {
childList: true,
subtree: true,
});
cleanNavBarCallback([]);
}
});
// This is the vertical nav bar at the left of the page
waitForKeyElements('nav[id="VerticalNavContent"]', function (nodes) {
console.debug("Cleaning vertical nav bar");
if (nodes && nodes.length > 0) {
console.debug("Cleaning vertical nav bar:", nodes[0]);
cleanVerticalNavBar(nodes[0]);
}
});
}
// DECLUTTER PINS
function cleanShoppingAds(pins) {
const shoppingAdDivs = document.querySelectorAll(
"div.Ch2.zDA.IZT.CKL.tBJ.dyH.iFc.GTB.H2s",
);
shoppingAdDivs.forEach((adDiv) => {
let parent = adDiv.closest('div[role="listitem"]');
hideElement(parent);
console.debug("Removed shopping container");
});
}
function cleanIdeasYouMightLove(pins) {
pins.forEach((pin) => {
if (
pin.textContent.toLowerCase().includes("ideas you might love") ||
pin.textContent.toLowerCase().includes("shop similar") ||
pin.textContent.toLowerCase().includes("shop featured boards")
) {
hideElement(pin);
console.debug('Removed "Ideas you might love" item:', pin);
}
});
}
function removePinFooters(pins) {
if (!SETTINGS.removePinFooters.currentValue()) {
return;
}
pins.forEach((pin) => {
const footer = pin.querySelector('div[data-test-id="pinrep-footer"]');
if (!footer) {
return;
}
hideElement(footer);
console.debug("Removed pin footer:", footer);
});
}
/**
* Remove Shoppable Pins by looking for a little tag ("Shoppable Pin indicator") in the pin somewhere.
* Shoppable pin indicators could be in the footer, or as an svg on top of the image
*/
function removeShoppablePins(pins) {
if (!SETTINGS.removeShoppablePins.currentValue()) {
return;
}
pins.forEach((pin) => {
const shoppableIndicator = pin.querySelector(
'[aria-label="Shoppable Pin indicator"]',
);
if (shoppableIndicator) {
hideElement(pin);
console.debug("Removed shoppable pin:", pin);
}
});
}
function removeSponsoredPins(pins, mutations) {
var promotedPinSelector =
'a[aria-label="Promoted by"], a[aria-label="Promoted by; Opens a new tab"], div[title="Sponsored"]';
pins.forEach((pin) => {
if (pin && pin.querySelector(promotedPinSelector)) {
hideElement(pin);
}
});
}
/**
* Pinterest now has a "Shop now" module as the first result of some searches.
* We will remove this.
*/
function removeShoppingModule(pins) {
console.debug("Cleaning shopping modules");
if (pins) {
pins.forEach((pin) => {
var module = pin.querySelector("div.Module");
if (!module || !module.innerText) {
return;
}
const shoppingModuleTexts = new Set(["shop now", "continue shopping"]);
const innerTextLines = module.innerText
.trim()
.split("\n")
.map((x) => x.trim().toLowerCase());
for (var i = 0; i < innerTextLines.length; i++) {
if (shoppingModuleTexts.has(innerTextLines[i])) {
hideElement(pin);
console.debug("Removed shopping module");
break;
}
}
});
}
}
/**
* Pinterest now is promoting their own shopping boards (from the "Pinterest Shop" user). We
* will remove those.
*/
function removeFeaturedBoards(pins) {
if (pins) {
pins.forEach((pin) => {
if (pin.style.display === "none") {
return;
}
if (pin.querySelector('[data-test-id="pinRepPresentation"]')) {
return;
}
var innerText = pin.innerText ? pin.innerText.trim().toLowerCase() : "";
var splitText = innerText.split("\n").map((x) => x.trim().toLowerCase());
if (
splitText.includes("explore featured boards") ||
splitText.includes("pinterest shop")
) {
hideElement(pin);
console.debug("Removed featured boards module");
}
});
}
}
function cleanRelatedPinsSection(mutations) {
console.debug("Cleaning related pins");
var pins = document.querySelectorAll('div[role="listitem"]');
cleanIdeasYouMightLove(pins);
cleanShoppingAds(pins);
removePinFooters(pins);
removeShoppablePins(pins);
removeSponsoredPins(pins, mutations);
removeShoppingModule(pins);
removeFeaturedBoards(pins);
}
function cleanProductListingPinPage() {
waitAndRemove('div[data-test-id="product-price"]');
waitAndRemove('div[data-test-id="pdp-product-metadata-domain-owner"]');
waitAndRemove(
'div[data-test-id="product-shop-button"]',
(elem) => (elem.parentElement.style.display = "none"),
);
waitAndRemove(
'div[data-test-id="product-description"]',
(elem) => (elem.parentElement.style.display = "none"),
);
}
function cleanPinVisualContainer() {
var pinVisualContainer = document.querySelector(
'div[data-test-id="closeup-visual-container"]',
);
if (pinVisualContainer) {
var shopButton = pinVisualContainer.querySelector(
'div[data-test-id="experimental-closeup-image-overlay-layer-shop-button"]',
);
hideElement(shopButton);
var domainLinkButton = pinVisualContainer.querySelector(
'div[data-test-id="experimental-closeup-image-overlay-layer-domain-link-button"]'
);
hideElement(domainLinkButton);
var closeupImageOverlay = pinVisualContainer.querySelector(
'div[data-test-id="closeup-image-overlay-layer-domain-link-button"]'
);
hideElement(closeupImageOverlay);
}
}
/**
* The collapsible layout seems to be only used for the "Shop the look" section.
* So we will remove it.
*/
function removeShopTheLookSection() {
// 1. CSS rule as a backup (will not match text, but can help if Pinterest adds a class or data attribute in the future)
const style = document.createElement('style');
style.textContent = `
/* This will hide the whole section if you can add a class or data attribute in the future */
/* For now, we rely on JS for text matching */
`;
document.head.appendChild(style);
// 2. Function to hide all "Shop the look" sections
function hideShopTheLookSection() {
document.querySelectorAll('div[data-test-id="collapsible-layout"]').forEach(function(layout) {
// Look for any h2 or h3 with text "Shop the look" (case-insensitive)
const headings = layout.querySelectorAll('h2, h3');
for (const heading of headings) {
if (heading.textContent && heading.textContent.trim().toLowerCase() === 'shop the look') {
hideElement(layout);
console.debug('Removed Shop the look section (robust observer)');
break;
}
}
});
}
// Initial hide
hideShopTheLookSection();
// 3. Observe the whole body for new nodes
const observer = new MutationObserver(hideShopTheLookSection);
observer.observe(document.querySelector('div[role="list"]'), { childList: true, subtree: true });
}
function cleanComments() {
if (SETTINGS.removeComments.currentValue()) {
waitForKeyElements("#canonical-card", function (canonicalCards) {
if (canonicalCards && canonicalCards.length == 1) {
var canonicalCard = canonicalCards[0];
if (!canonicalCard) {
return;
}
var hasCommentsHeading =
canonicalCard.querySelector("#comments-heading") != null;
if (hasCommentsHeading) {
hideElement(canonicalCard);
console.debug("Removed comments section from pin");
}
}
});
waitForKeyElements(
'div[data-test-id="inline-comment-composer-container"]',
function (nodes) {
var commentBox = nodes[0];
if (commentBox) {
hideElement(commentBox);
console.debug("Removed comment box from pin");
}
},
);
}
}
function cleanPinDescriptionContainer() {
var pinDescriptionContainer = document.querySelector(
'div[data-test-id="description-content-container"]',
);
if (pinDescriptionContainer) {
var shopButton = pinDescriptionContainer.querySelector(
'div[data-test-id="product-shop-button"]',
);
hideElement(shopButton);
// Remove the new Shop the look section
var collapsibleLayouts = document.querySelectorAll('div[data-test-id="collapsible-layout"]');
collapsibleLayouts.forEach(function(layout) {
var heading = layout.querySelector('h2#comments-heading');
if (heading && heading.textContent.trim().toLowerCase() === 'shop the look') {
hideElement(layout);
console.debug('Removed Shop the look section');
}
});
}
}
/**
* Pinterest adds the number of notifications to the title
* in a distracting, flashing manner like Pinerest (2).
* So this will keep the page title at: Pinterest
*/
function enforceTitle() {
if (document.title !== ORIGINAL_TITLE) {
console.debug(
`Changing title from: ${document.title} to ${ORIGINAL_TITLE}`,
);
document.title = ORIGINAL_TITLE;
}
}
/**
* Pinterest now has a popup modal asking you to disable adblock, if it is present.
*
* We will remove this modal.
*/
function removeAntiAdblockModalIfExists() {
console.debug("Waiting for anti-Adblock modal");
waitForKeyElements('div[aria-label="Ad blocker modal"]', function (nodes) {
nodes[0]?.remove();
document.querySelector('div[name="trap-focus"]')?.remove();
document.body.style.overflow = "auto";
console.debug("Removed anti-Adblock modal");
});
}
// PAGE IDENTIFIERS
function isProductPin() {
return document.getElementById("product-sticky-container ") != null;
}
function hasDescriptionContentContainer() {
return (
document.querySelector(
'div[data-test-id="description-content-container"]',
) != null
);
}
function isBoard() {
return document.querySelector('div[data-test-id="board-header"]') != null;
}
function observeSidebarForExploreBadge() {
// Find the sidebar navigation container (adjust selector if needed)
const sidebarNav = document.querySelector('nav[id="VerticalNavContent"]') || document.querySelector('div[role="navigation"]');
if (!sidebarNav) {
// Try again later if sidebar not yet loaded
setTimeout(observeSidebarForExploreBadge, 500);
return;
}
// Remove any existing badge immediately
removeExploreTabNotificationsIcon();
// Set up observer
const observer = new MutationObserver(() => {
removeExploreTabNotificationsIcon();
});
observer.observe(sidebarNav, { childList: true, subtree: true });
}
function main() {
"use strict";
console.debug("Running main()");
updateSettingsMenu();
cleanNavBar();
cleanComments();
cleanNavTabCarousel();
removeAntiAdblockModalIfExists();
if (isProductPin()) {
cleanProductListingPinPage();
}
cleanShopButtonsFromBoard();
cleanShopByBanners();
// The nav tab tab carousel pops up after page load
waitForKeyElements('div[class="localNavTabCarousel"]', function (nodes) {
cleanNavTabCarousel();
});
cleanPinDescriptionContainer();
removeShopTheLookSection();
cleanPinVisualContainer();
// The pin visual container changes to add those little "shop" buttons
// over the top of the image, so we need to watch for those and remove them
waitForKeyElements(
'div[data-test-id="closeup-visual-container"]',
function (nodes) {
new MutationObserver((mutations, observer) => {
cleanPinVisualContainer();
}).observe(nodes[0], {
childList: true,
subtree: true,
});
},
);
const relatedPinsSectionCleanerObserver = new MutationObserver(
(mutations) => {
cleanRelatedPinsSection(mutations);
},
);
waitForKeyElements('div[role="list"]', function (node) {
// Related pins section must be watched for changes,
// as new pins pop up as the user scrolls
cleanRelatedPinsSection([]);
relatedPinsSectionCleanerObserver.observe(node[0], {
childList: true,
});
});
enforceTitle();
const titleElement = document.querySelector("title");
new MutationObserver(enforceTitle).observe(titleElement, { childList: true });
observeSidebarForExploreBadge();
}
main();
let lastUrl = window.location.href;
setInterval(() => {
const currentUrl = window.location.href;
if (currentUrl !== lastUrl) {
console.debug(
`Detected new page, currentURL=${currentUrl}, previousURL=${lastUrl}`,
);
lastUrl = currentUrl;
main();
}
}, 750);